diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0b71dc853..bed019e65 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,8 +28,8 @@ jobs: pull-requests: write issues: write outputs: - staged: ${{ steps.changesets.outputs.staged }} - staged_packages: ${{ steps.changesets.outputs.staged_packages }} + staged: ${{ steps.release.outputs.staged }} + staged_packages: ${{ steps.release.outputs.staged_packages }} steps: - name: Check out code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 @@ -54,27 +54,17 @@ jobs: env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - # createGithubReleases stays off: a staged version isn't public until - # `promote` confirms npm approval, so nothing should announce it yet. - name: Release - id: changesets - uses: changesets/action@63a615b9cd06ba9a3e6d13796c7fbcb080a60a0b # v1 + id: release + uses: kubb-labs/config/.github/actions/release@6498246dc71ad3c27db69f9cc1f928a05e79d0c7 # main with: - version: pnpm exec changeset version - publish: 'node scripts/release.mjs' - commit: 'ci(changesets): version packages' - title: 'ci(changesets): version packages' - setupGitUser: false - createGithubReleases: false - env: - NPM_CONFIG_PROVENANCE: true - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + github-token: ${{ secrets.GITHUB_TOKEN }} # Canary skips staging on purpose, so it stays automatic on every push. # If the npm Trusted Publisher for these packages is ever locked to # stage-only, this step will need its own non-stage-restricted entry. - name: Publish canary - if: ${{ steps.changesets.outputs.staged != 'true' && github.event_name == 'push' && github.ref == 'refs/heads/main' }} + if: ${{ steps.release.outputs.staged != 'true' && github.event_name == 'push' && github.ref == 'refs/heads/main' }} continue-on-error: true shell: bash env: @@ -103,7 +93,7 @@ jobs: permissions: contents: write # gh release create outputs: - approved: ${{ steps.verify.outputs.approved }} + approved: ${{ steps.promote.outputs.approved }} steps: - name: Check out code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 @@ -116,38 +106,17 @@ jobs: node-version: '22.22.3' cache: 'false' - # A green environment review only means a maintainer clicked approve, - # not that they actually ran `npm stage approve`. This confirms the - # staged versions are really live on the registry before anything - # public fires. - - name: Verify staged versions are live on npm - id: verify - run: node scripts/verifyRegistry.mjs - env: - STAGED_PACKAGES: ${{ needs.release.outputs.staged_packages }} - # How often to re-poll npm while waiting for a staged version to - # propagate, and how many times to try before giving up. - VERIFY_INTERVAL_SECONDS: '300' # in seconds - VERIFY_RETRY_ATTEMPTS: '10' - - # Tags are created here, not at stage time, so a rejected or expired - # staged package never leaves a dangling tag behind (see - # changesets/changesets#2025). The versions are already confirmed live - # above, so tagging now always points at something real. - - name: Tag released versions - run: | - pnpm exec changeset tag - git push --tags - # Every staged package gets its own release here, since packages in # this repo version and changelog independently (see the empty `fixed` # and `linked` groups in .changeset/config.json) — unlike kubb-labs/kubb, - # where every package shares one fixed version and one combined release. - - name: Create GitHub Releases - run: node scripts/createReleases.mjs - env: - STAGED_PACKAGES: ${{ needs.release.outputs.staged_packages }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # where every package shares one fixed version and one combined release, + # so this omits release-mode (defaults to per-package). + - name: Promote + id: promote + uses: kubb-labs/config/.github/actions/promote@6498246dc71ad3c27db69f9cc1f928a05e79d0c7 # main + with: + staged-packages: ${{ needs.release.outputs.staged_packages }} + github-token: ${{ secrets.GITHUB_TOKEN }} deploy-platform: name: Dispatch content-updated to platform diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 97e52c2e2..f83ed81a9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -175,7 +175,7 @@ To approve a release: 2. The same maintainer approves the `promote` job's environment review on the workflow run in the Actions tab. 3. The `promote` job then verifies the versions are actually live on npm, tags the released versions, and creates a GitHub Release, and only then dispatches the content refresh to [kubb-labs/platform](https://github.com/kubb-labs/platform). -Packages in this repo version and changelog independently (see the empty `fixed` and `linked` groups in `.changeset/config.json`), so a release here creates one GitHub Release per staged package, tagged `@`, with notes taken from that package's own `CHANGELOG.md`. This differs from [kubb-labs/kubb](https://github.com/kubb-labs/kubb), where every package shares one fixed version and a release covers all of them combined. `scripts/createReleases.mjs` is shared verbatim between the two repos; the `RELEASE_MODE` environment variable picks the mode, and this repo simply never sets it to `combined`. +Packages in this repo version and changelog independently (see the empty `fixed` and `linked` groups in `.changeset/config.json`), so a release here creates one GitHub Release per staged package, tagged `@`, with notes taken from that package's own `CHANGELOG.md`. This differs from [kubb-labs/kubb](https://github.com/kubb-labs/kubb), where every package shares one fixed version and a release covers all of them combined. The `release` and `promote` steps come from the shared [kubb-labs/config](https://github.com/kubb-labs/config) actions (`.github/actions/release` and `.github/actions/promote`); this repo simply never sets the `promote` action's `release-mode` input to `combined`. If a staged version turns out to be wrong, reject it with `npm stage reject` instead of approving it. Nothing downstream fires for a rejected version. diff --git a/internals/changelog/format.mjs b/internals/changelog/format.mjs deleted file mode 100644 index 5e17fc5cf..000000000 --- a/internals/changelog/format.mjs +++ /dev/null @@ -1,20 +0,0 @@ -// The heading grammar shared by the changelog writer (index.mjs) and anything -// that reads CHANGELOG.md back out (scripts/createReleases.mjs). Kept in one -// place so the two can't drift into two independently-escaped, independently -// anchored regexes for the same headings. - -function escapeForRegExp(value) { - return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') -} - -// The lookahead after the version rejects a longer, hyphen-suffixed version -// sharing the same numeric prefix (e.g. searching for "5.0.0" must not match -// a "## v5.0.0-rc.1" heading), while still matching the exact version -// followed by whitespace, a separator, or end of line. -export function versionHeadingPattern(version) { - return new RegExp(`^##\\s+v?${escapeForRegExp(version)}(?![\\w.-]).*$`, 'm') -} - -export function packageHeadingPattern(name) { - return new RegExp(`^###\\s+${escapeForRegExp(name)}\\s*$`, 'm') -} diff --git a/internals/changelog/format.test.mjs b/internals/changelog/format.test.mjs deleted file mode 100644 index cecb12006..000000000 --- a/internals/changelog/format.test.mjs +++ /dev/null @@ -1,26 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { packageHeadingPattern, versionHeadingPattern } from './format.mjs' - -describe('versionHeadingPattern', () => { - it('matches the exact version heading', () => { - expect(versionHeadingPattern('5.0.0-beta.99').test('## 5.0.0-beta.99')).toBe(true) - }) - - it('does not match a longer version sharing the same numeric prefix', () => { - expect(versionHeadingPattern('5.0.0').test('## v5.0.0-rc.1')).toBe(false) - }) - - it('matches a bare version with no prerelease suffix', () => { - expect(versionHeadingPattern('5.0.0').test('## 5.0.0')).toBe(true) - }) -}) - -describe('packageHeadingPattern', () => { - it('matches an exact package heading', () => { - expect(packageHeadingPattern('@kubb/plugin-axios').test('### @kubb/plugin-axios')).toBe(true) - }) - - it('does not match a different package heading with a shared prefix', () => { - expect(packageHeadingPattern('@kubb/plugin-ts').test('### @kubb/plugin-ts-extra')).toBe(false) - }) -}) diff --git a/scripts/createReleases.mjs b/scripts/createReleases.mjs deleted file mode 100644 index e6ce05b99..000000000 --- a/scripts/createReleases.mjs +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env node -import { spawnSync } from 'node:child_process' -import { readFileSync } from 'node:fs' -import { packageHeadingPattern, versionHeadingPattern } from '../internals/changelog/format.mjs' - -// The custom changelog hook (internals/changelog/index.mjs) writes one root -// CHANGELOG.md with a `## v` heading per release and a `### ` -// subsection per package inside it. This finds that whole version block, the -// shared starting point for both a combined, whole-version release and a -// single package's own subsection within it. -export function extractVersionNotes({ changelog, version }) { - const versionMatch = versionHeadingPattern(version).exec(changelog) - if (!versionMatch) return null - - const afterVersion = changelog.slice(versionMatch.index + versionMatch[0].length) - const nextVersionHeading = /^##\s+/m.exec(afterVersion) - return (nextVersionHeading ? afterVersion.slice(0, nextVersionHeading.index) : afterVersion).trim() -} - -// Reproduces what changesets/action would normally show for one package, -// sourced from this repo's aggregated CHANGELOG.md instead of a per-package -// changelog. -export function extractPackageNotes({ changelog, name, version }) { - const versionBlock = extractVersionNotes({ changelog, version }) - if (versionBlock === null) return null - - const packageMatch = packageHeadingPattern(name).exec(versionBlock) - if (!packageMatch) return null - - const afterPackage = versionBlock.slice(packageMatch.index + packageMatch[0].length) - const nextHeading = /^#{2,3}\s+/m.exec(afterPackage) - const packageBlock = nextHeading ? afterPackage.slice(0, nextHeading.index) : afterPackage - - return packageBlock.trim() || null -} - -function releaseExists(tag) { - return spawnSync('gh', ['release', 'view', tag], { encoding: 'utf8' }).status === 0 -} - -// A re-run of `promote` (e.g. a manual `promote: true` dispatch against a -// version that was already tagged and released) would otherwise fail here: -// `gh release create` errors on a tag that already has a release. Skipping -// cleanly makes promote safe to re-run instead of going red on a re-dispatch. -function createRelease({ tag, title, notes }) { - if (releaseExists(tag)) { - console.log(`Release ${tag} already exists, skipping.`) - return - } - - const result = spawnSync('gh', ['release', 'create', tag, '--title', title, '--notes', notes], { stdio: 'inherit' }) - if (result.status !== 0) process.exit(result.status ?? 1) -} - -function getWorkspacePaths() { - const result = spawnSync('pnpm', ['ls', '-r', '--json', '--depth', '0'], { encoding: 'utf8' }) - if (result.status !== 0) return {} - try { - return Object.fromEntries(JSON.parse(result.stdout).map((pkg) => [pkg.name, pkg.path])) - } catch { - return {} - } -} - -function createPerPackageReleases({ staged, repo }) { - const pkgPaths = getWorkspacePaths() - for (const pkg of staged) { - let notes = null - const pkgPath = pkgPaths[pkg.name] - if (pkgPath) { - try { - notes = extractVersionNotes({ changelog: readFileSync(`${pkgPath}/CHANGELOG.md`, 'utf8'), version: pkg.version }) - } catch {} - } - notes ??= `Dependency update only, no direct changes for this package. See [CHANGELOG.md](https://github.com/${repo}/blob/main/CHANGELOG.md) for the full release notes.` - createRelease({ tag: `${pkg.name}@${pkg.version}`, title: `${pkg.name}@${pkg.version}`, notes }) - } -} - -// One release for the whole version, tagged with the flagship package's own -// tag (already created by the `changeset tag` step) rather than inventing a -// second, parallel tagging scheme. -function createCombinedRelease({ staged, changelog, repo }) { - const flagship = staged.find((pkg) => pkg.name === 'kubb') ?? staged[0] - if (!flagship) return - - const notes = - extractVersionNotes({ changelog, version: flagship.version }) ?? - `Dependency update only, no direct changes for this package. See [CHANGELOG.md](https://github.com/${repo}/blob/main/CHANGELOG.md) for the full release notes.` - createRelease({ tag: `${flagship.name}@${flagship.version}`, title: `v${flagship.version}`, notes }) -} - -function main() { - const staged = JSON.parse(process.env.STAGED_PACKAGES || '[]') - if (staged.length === 0) { - console.error('STAGED_PACKAGES is empty. The promote job only runs when release staged something, so this should never happen.') - process.exit(1) - } - - const repo = process.env.GITHUB_REPOSITORY - - if (process.env.RELEASE_MODE === 'combined') { - createCombinedRelease({ staged, changelog: readFileSync('CHANGELOG.md', 'utf8'), repo }) - } else { - createPerPackageReleases({ staged, repo }) - } -} - -if (import.meta.url === `file://${process.argv[1]}`) { - main() -} diff --git a/scripts/createReleases.test.mjs b/scripts/createReleases.test.mjs deleted file mode 100644 index d27584571..000000000 --- a/scripts/createReleases.test.mjs +++ /dev/null @@ -1,81 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { extractPackageNotes, extractVersionNotes } from './createReleases.mjs' - -const changelog = `# Changelog - -## v5.0.0-beta.80 — Jul 2, 2026 - -### @kubb/ast - -#### Features - -- Let a printer node override reuse the handler it replaces. - -### @kubb/parser-md - -#### Bug Fixes - -- Correct the \`parserMd\` JSDoc. - -### Contributors - -Thanks to everyone who contributed to this release: - -[@stijnvanhulle](https://github.com/stijnvanhulle) - -## v5.0.0-beta.79 — Jun 30, 2026 - -### @kubb/cli - -#### Bug Fixes - -- Cleanup defineLogger -` - -describe('extractPackageNotes', () => { - it('extracts a package section from its version block', () => { - expect(extractPackageNotes({ changelog, name: '@kubb/ast', version: '5.0.0-beta.80' })).toBe( - '#### Features\n\n- Let a printer node override reuse the handler it replaces.', - ) - }) - - it('stops at the next package heading in the same version block', () => { - expect(extractPackageNotes({ changelog, name: '@kubb/parser-md', version: '5.0.0-beta.80' })).toBe('#### Bug Fixes\n\n- Correct the `parserMd` JSDoc.') - }) - - it('does not reach into an older version block for the same package', () => { - expect(extractPackageNotes({ changelog, name: '@kubb/cli', version: '5.0.0-beta.80' })).toBeNull() - }) - - it('finds a package in an older version block by its own version', () => { - expect(extractPackageNotes({ changelog, name: '@kubb/cli', version: '5.0.0-beta.79' })).toBe('#### Bug Fixes\n\n- Cleanup defineLogger') - }) - - it('returns null when the version heading does not exist', () => { - expect(extractPackageNotes({ changelog, name: '@kubb/ast', version: '9.9.9' })).toBeNull() - }) - - it('returns null when the package has no section in that version', () => { - expect(extractPackageNotes({ changelog, name: '@kubb/core', version: '5.0.0-beta.80' })).toBeNull() - }) - - it('does not match a longer prerelease version sharing the same numeric prefix', () => { - const withPrerelease = `# Changelog\n\n## v5.0.0-rc.1 — Jul 3, 2026\n\n### @kubb/ast\n\n#### Features\n\n- Something.\n` - expect(extractPackageNotes({ changelog: withPrerelease, name: '@kubb/ast', version: '5.0.0' })).toBeNull() - }) -}) - -describe('extractVersionNotes', () => { - it('returns the whole version block, covering every package in it', () => { - const notes = extractVersionNotes({ changelog, version: '5.0.0-beta.80' }) - - expect(notes).toContain('### @kubb/ast') - expect(notes).toContain('### @kubb/parser-md') - expect(notes).toContain('### Contributors') - expect(notes).not.toContain('v5.0.0-beta.79') - }) - - it('returns null when the version heading does not exist', () => { - expect(extractVersionNotes({ changelog, version: '9.9.9' })).toBeNull() - }) -}) diff --git a/scripts/release.mjs b/scripts/release.mjs deleted file mode 100644 index d39ba56fa..000000000 --- a/scripts/release.mjs +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env node -import { spawnSync } from 'node:child_process' -import { appendFileSync, readFileSync } from 'node:fs' - -function readPreTag() { - try { - return JSON.parse(readFileSync('.changeset/pre.json', 'utf8')).tag ?? null - } catch { - return null - } -} - -// Parse pnpm stage publish output into [{ name, version }]. Prefers the --json -// payload, in whichever shape it comes in (a bare array, or an object keyed by -// package name, both observed across pnpm/npm staged-publish responses), then -// falls back to scanning text output for `+ @` lines. -export function parseStaged(output) { - try { - const parsed = JSON.parse(output) - const entries = Array.isArray(parsed) - ? parsed - : parsed && typeof parsed === 'object' - ? Object.entries(parsed).map(([key, value]) => ({ name: value?.name ?? value?.packageName ?? key, version: value?.version })) - : null - - if (entries) { - return entries.filter((entry) => entry?.name && entry?.version).map((entry) => ({ name: entry.name, version: entry.version })) - } - } catch {} - - const found = [] - for (const line of output.split('\n')) { - const match = line.match(/^\+\s+(@?[^\s@]+)@(\S+)/) - if (match) found.push({ name: match[1], version: match[2] }) - } - return found -} - -function main() { - const tag = readPreTag() - const stageArgs = ['stage', 'publish', '-r', '--no-git-check', '--access', 'public', '--json'] - if (tag) stageArgs.push('--tag', tag) - - const result = spawnSync('pnpm', stageArgs, { encoding: 'utf8' }) - if (result.stdout) process.stdout.write(result.stdout) - if (result.stderr) process.stderr.write(result.stderr) - if (result.status !== 0) process.exit(result.status ?? 1) - const output = result.stdout ?? '' - const staged = parseStaged(output) - - // Tags aren't created here. A staged package may still be rejected on npm, - // and changesets/changesets#2025 specifically warns against tagging at - // stage time for that reason. The `promote` job creates and pushes tags - // itself, after confirming the versions are actually live. - // - // Only signal a stage to the workflow if pnpm actually published something. - // When everything was skipped (versions already on npm), let the canary step - // fire instead — that's the intent for ordinary main pushes. - if (staged.length > 0 && process.env.GITHUB_OUTPUT) { - appendFileSync(process.env.GITHUB_OUTPUT, 'staged=true\n') - appendFileSync(process.env.GITHUB_OUTPUT, `staged_packages=${JSON.stringify(staged)}\n`) - } -} - -if (import.meta.url === `file://${process.argv[1]}`) { - main() -} diff --git a/scripts/release.test.mjs b/scripts/release.test.mjs deleted file mode 100644 index 68dc7f906..000000000 --- a/scripts/release.test.mjs +++ /dev/null @@ -1,53 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { parseStaged } from './release.mjs' - -describe('parseStaged', () => { - it('returns name/version pairs from a JSON array payload', () => { - const output = JSON.stringify([ - { name: '@kubb/plugin-axios', version: '5.0.0-beta.99' }, - { name: '@kubb/plugin-ts', version: '5.0.0-beta.99' }, - ]) - - expect(parseStaged(output)).toStrictEqual([ - { name: '@kubb/plugin-axios', version: '5.0.0-beta.99' }, - { name: '@kubb/plugin-ts', version: '5.0.0-beta.99' }, - ]) - }) - - it('drops JSON entries missing a name or version', () => { - const output = JSON.stringify([{ name: '@kubb/plugin-axios', version: '5.0.0-beta.99' }, { name: '@kubb/plugin-ts' }, { version: '1.0.0' }]) - - expect(parseStaged(output)).toStrictEqual([{ name: '@kubb/plugin-axios', version: '5.0.0-beta.99' }]) - }) - - it('returns name/version pairs from a JSON object keyed by package name', () => { - const output = JSON.stringify({ - '@kubb/plugin-axios': { name: '@kubb/plugin-axios', version: '5.0.0-beta.99' }, - '@kubb/plugin-ts': { name: '@kubb/plugin-ts', version: '5.0.0-beta.99' }, - }) - - expect(parseStaged(output)).toStrictEqual([ - { name: '@kubb/plugin-axios', version: '5.0.0-beta.99' }, - { name: '@kubb/plugin-ts', version: '5.0.0-beta.99' }, - ]) - }) - - it('falls back to the object key as the name when an entry has neither a name nor a packageName field', () => { - const output = JSON.stringify({ '@kubb/plugin-axios': { version: '5.0.0-beta.99' } }) - - expect(parseStaged(output)).toStrictEqual([{ name: '@kubb/plugin-axios', version: '5.0.0-beta.99' }]) - }) - - it('falls back to scanning `+ @` lines when the payload is not JSON', () => { - const output = ['Packages: +2', '+ @kubb/plugin-axios@5.0.0-beta.99', '+ @kubb/plugin-ts@5.0.0-beta.99', 'Done'].join('\n') - - expect(parseStaged(output)).toStrictEqual([ - { name: '@kubb/plugin-axios', version: '5.0.0-beta.99' }, - { name: '@kubb/plugin-ts', version: '5.0.0-beta.99' }, - ]) - }) - - it('returns an empty array when nothing was staged', () => { - expect(parseStaged('No packages to publish')).toStrictEqual([]) - }) -}) diff --git a/scripts/verifyRegistry.mjs b/scripts/verifyRegistry.mjs deleted file mode 100644 index ff97c1c33..000000000 --- a/scripts/verifyRegistry.mjs +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env node -import { spawnSync } from 'node:child_process' -import { appendFileSync } from 'node:fs' - -const RETRY_ATTEMPTS = Number(process.env.VERIFY_RETRY_ATTEMPTS) || 6 -const RETRY_DELAY_MS = (Number(process.env.VERIFY_INTERVAL_SECONDS) || 10) * 1_000 - -function isLiveOnRegistry(pkg) { - const result = spawnSync('npm', ['view', `${pkg.name}@${pkg.version}`, '--json'], { encoding: 'utf8' }) - if (result.status !== 0) return false - - try { - const parsed = JSON.parse(result.stdout) - return Boolean(parsed?.version) - } catch { - return false - } -} - -// A green environment review only means a maintainer clicked approve, not -// that they actually ran `npm stage approve`. Retrying absorbs registry -// propagation lag right after a real approval; giving up after all attempts -// means the version genuinely isn't live yet. -export async function verifyPackage({ pkg, attempts = RETRY_ATTEMPTS, delayMs = RETRY_DELAY_MS }) { - for (let attempt = 1; attempt <= attempts; attempt++) { - if (isLiveOnRegistry(pkg)) return true - - if (attempt < attempts) { - process.stdout.write(`${pkg.name}@${pkg.version} not live yet (attempt ${attempt}/${attempts}), retrying in ${delayMs / 1_000}s ...\n`) - await new Promise((r) => setTimeout(r, delayMs)) - } - } - - return false -} - -async function main() { - const staged = JSON.parse(process.env.STAGED_PACKAGES || '[]') - if (staged.length === 0) { - console.error('STAGED_PACKAGES is empty. The promote job only runs when release staged something, so this should never happen.') - process.exit(1) - } - - // Each package's retries run independently, so verifying them concurrently - // instead of one after another keeps the total wait close to the slowest - // single package's retry budget, not the sum across every staged package. - const results = await Promise.all( - staged.map(async (pkg) => { - process.stdout.write(`Checking ${pkg.name}@${pkg.version} ...\n`) - const live = await verifyPackage({ pkg }) - return { pkg, live } - }), - ) - - const missing = results.filter((result) => !result.live) - if (missing.length > 0) { - const waitedSeconds = ((RETRY_ATTEMPTS - 1) * RETRY_DELAY_MS) / 1_000 - for (const { pkg } of missing) { - console.error( - `${pkg.name}@${pkg.version} isn't live on npm yet after ${RETRY_ATTEMPTS} attempts over ${waitedSeconds}s. Confirm npm stage approve was run for this exact version, or that the staged version hasn't expired.`, - ) - } - process.exit(1) - } - - if (process.env.GITHUB_OUTPUT) { - appendFileSync(process.env.GITHUB_OUTPUT, 'approved=true\n') - } -} - -if (import.meta.url === `file://${process.argv[1]}`) { - await main() -} diff --git a/scripts/verifyRegistry.test.mjs b/scripts/verifyRegistry.test.mjs deleted file mode 100644 index af83979b1..000000000 --- a/scripts/verifyRegistry.test.mjs +++ /dev/null @@ -1,44 +0,0 @@ -import { spawnSync } from 'node:child_process' -import { describe, expect, it, vi } from 'vitest' -import { verifyPackage } from './verifyRegistry.mjs' - -// node:child_process is a built-in with non-configurable exports, so vi.spyOn -// can't patch it directly in real ESM. vi.mock is the only way to replace it; -// each test still owns its own return-value sequence via mockReturnValueOnce, -// so there's no shared state or beforeEach reset needed between tests. -vi.mock('node:child_process', () => ({ spawnSync: vi.fn() })) - -const pkg = { name: '@kubb/plugin-axios', version: '5.0.0-beta.99' } - -function mockView(results) { - const mocked = vi.mocked(spawnSync) - mocked.mockClear() - for (const result of results) mocked.mockReturnValueOnce(result) - return mocked -} - -const live = { status: 0, stdout: JSON.stringify({ version: pkg.version }) } -const missing = { status: 1, stdout: '' } - -describe('verifyPackage', () => { - it('returns true when the version is live on the first attempt', async () => { - const mocked = mockView([live]) - - await expect(verifyPackage({ pkg, attempts: 3, delayMs: 0 })).resolves.toBe(true) - expect(mocked).toHaveBeenCalledTimes(1) - }) - - it('retries until the version shows up on the registry', async () => { - const mocked = mockView([missing, missing, live]) - - await expect(verifyPackage({ pkg, attempts: 5, delayMs: 0 })).resolves.toBe(true) - expect(mocked).toHaveBeenCalledTimes(3) - }) - - it('returns false after exhausting every attempt', async () => { - const mocked = mockView([missing, missing]) - - await expect(verifyPackage({ pkg, attempts: 2, delayMs: 0 })).resolves.toBe(false) - expect(mocked).toHaveBeenCalledTimes(2) - }) -})