diff --git a/scripts/generate-changeset-feedback.js b/scripts/generate-changeset-feedback.js deleted file mode 100644 index b9ad09ca72..0000000000 --- a/scripts/generate-changeset-feedback.js +++ /dev/null @@ -1,314 +0,0 @@ -/* - * Copyright 2022 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -const fs = require('fs'); -const { execFile: execFileCb } = require('child_process'); -const { promisify } = require('util'); -const { - basename, - resolve: resolvePath, - relative: relativePath, -} = require('path'); - -const execFile = promisify(execFileCb); - -// Tells whether a path relative to the package directory has an effect -// on the published package -function isPublishedPath(path) { - if (path.startsWith('dev/')) { - return false; - } - if (path.includes('__mocks__')) { - return false; - } - if (path.includes('__fixtures__')) { - return false; - } - // Don't count manual modifications to the changelog - if (path === 'CHANGELOG.md') { - return false; - } - // API report changes by themselves don't count - if (path === 'api-report.md' || path === 'cli-report.md') { - return false; - } - // Lint changes don't count - if (path === '.eslintrc.js') { - return false; - } - - const name = basename(path); - if (name.startsWith('setupTests.')) { - return false; - } - if (name.includes('.test.')) { - return false; - } - if (name.includes('.stories.')) { - return false; - } - return true; -} - -async function listChangedFiles(ref) { - if (!ref) { - throw new Error('ref is required'); - } - - const { stdout } = await execFile('git', ['diff', '--name-only', ref]); - return stdout.trim().split(/\r?\n/); -} - -async function listPackages() { - const rootPkg = require(resolvePath(process.cwd(), './package.json')); - if (!rootPkg?.workspaces?.packages) { - throw new Error('No workspaces found in root package.json'); - } - - const pkgs = []; - - // Naive workspace lookup implementation, we can't shell out to yarn here as the implementation embedded in the repo - for (const pkgPath of rootPkg?.workspaces?.packages) { - const readDirRecursive = (dir, parts) => { - const [nextPart] = parts; - - // We've reached the end of the path pattern, check if package.json exists - if (!nextPart) { - try { - const pkg = require(resolvePath(dir, 'package.json')); - pkgs.push({ - path: relativePath(process.cwd(), dir), - name: pkg.name, - }); - } catch { - process.stderr.write(`Failed to read package.json in ${dir}\n`); - } - return; - } - - for (const filePath of fs.readdirSync(dir)) { - if (fs.statSync(resolvePath(dir, filePath)).isDirectory()) { - if (filePath === nextPart || nextPart === '*') { - readDirRecursive(resolvePath(dir, filePath), parts.slice(1)); - } - } - } - }; - - // Split the workspace paths by / and check each directory level recursively - readDirRecursive(process.cwd(), pkgPath.split('/')); - } - - return pkgs; -} - -async function loadChangesets(filePaths) { - const changesets = []; - for (const filePath of filePaths) { - if (!filePath.startsWith('.changeset/') || !filePath.endsWith('.md')) { - continue; - } - try { - const content = await fs.promises.readFile(filePath, 'utf8'); - let lines = content.split(/\r?\n/); - - lines = lines.slice(lines.findIndex(line => line === '---') + 1); - lines = lines.slice( - 0, - lines.findIndex(line => line === '---'), - ); - - const bumps = new Map(); - bumps.toJSON = () => Object.fromEntries(bumps); - for (const line of lines) { - const match = line.match(/^'(.*)': (patch|minor|major)$/); - if (!match) { - throw new Error(`Invalid changeset line: ${line}`); - } - - bumps.set(match[1], match[2]); - } - - changesets.push({ - filePath, - bumps, - }); - } catch (error) { - if (error.code !== 'ENOENT') { - throw error; - } - } - } - - return changesets; -} - -async function listChangedPackages(changedFiles, packages) { - const changedPackageMap = new Map(); - for (const filePath of changedFiles) { - for (const pkg of packages) { - if (filePath.startsWith(`${pkg.path}/`)) { - const pkgPath = relativePath(pkg.path, filePath); - if (!isPublishedPath(pkgPath)) { - break; - } - const entry = changedPackageMap.get(pkg.name); - if (entry) { - entry.files.push(pkgPath); - } else { - const pkgJson = require(resolvePath(pkg.path, 'package.json')); - - changedPackageMap.set(pkg.name, { - ...pkg, - version: pkgJson.version, - isStable: !pkgJson.version.startsWith('0.'), - isPrivate: Boolean(pkgJson.private), - files: [pkgPath], - }); - } - break; - } - } - } - return Array.from(changedPackageMap.values()); -} - -function formatSection(prefix = [], generator, suffix = ['']) { - const lines = Array.from(generator()); - if (lines.length === 0) { - return ''; - } - - return [...[prefix].flat(), ...lines, ...[suffix].flat(), '', ''].join('\n'); -} - -function formatSummary(changedPackages, changesets) { - const changedNames = new Set(changedPackages.map(pkg => pkg.name)); - - let output = ''; - - output += formatSection( - `## Missing Changesets - -The following package(s) are changed by this PR but do not have a changeset: -`, - function* section() { - for (const pkg of changedPackages) { - if (changesets.some(c => c.bumps.get(pkg.name))) { - continue; - } - if (pkg.isPrivate) { - continue; - } - yield `- **${pkg.name}**`; - } - }, - ` -See [CONTRIBUTING.md](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md#creating-changesets) for more information about how to add changesets. -`, - ); - - output += formatSection( - `## Unexpected Changesets - -The following changeset(s) reference packages that have not been changed in this PR: -`, - function* section() { - for (const c of changesets) { - const missing = Array.from(c.bumps.keys()).filter( - b => !changedNames.has(b), - ); - if (missing.length > 0) { - yield `- **${c.filePath}**: ${missing.join(', ')}`; - } - } - }, - ` -Note that only changes that affect the published package require changesets, for example changes to tests and storybook stories do not require changesets. -`, - ); - - output += formatSection( - `## Unnecessary Changesets - -The following package(s) are private and do not need a changeset: -`, - function* section() { - for (const pkg of changedPackages) { - if (changesets.some(c => c.bumps.get(pkg.name)) && pkg.isPrivate) { - yield `- **${pkg.name}**`; - } - } - }, - ); - - output += formatSection( - `## Changed Packages - -| Package Name | Package Path | Changeset Bump | Current Version | -|:-------------|:-------------|:--------------:|:----------------|`, - function* section() { - const bumpMap = { - undefined: -1, - patch: 0, - minor: 1, - major: 2, - }; - - for (const pkg of changedPackages) { - const maxBump = - changesets - .map(c => c.bumps.get(pkg.name)) - .reduce( - (max, bump) => (bumpMap[bump] > bumpMap[max] ? bump : max), - undefined, - ) ?? 'none'; - yield `| ${pkg.name} | ${pkg.path} | **${maxBump}** | \`v${pkg.version}\` |`; - } - }, - ); - - return output; -} - -async function main() { - const [diffRef = 'origin/master'] = process.argv.slice(2); - const changedFiles = await listChangedFiles(diffRef); - const packages = await listPackages(); - - const changesets = await loadChangesets(changedFiles); - const changedPackages = await listChangedPackages(changedFiles, packages); - - process.stderr.write( - JSON.stringify( - { - changesets, - changedPackages, - }, - null, - 2, - ), - ); - process.stderr.write('\n'); - - const summary = formatSummary(changedPackages, changesets); - process.stdout.write(summary); -} - -main().catch(error => { - console.error(error.stack); - process.exit(1); -});