-
-
Notifications
You must be signed in to change notification settings - Fork 5
feat: GH action to remind developers to add release notes in CHANGELOG.md #57
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
9449ec2
feat: GH action to remind developers to add release notes in CHANGELO…
cryptodev-2s 3b9a609
fix: cli lint issue
cryptodev-2s 15bc879
feat: get pr labels as inputs
cryptodev-2s 3dd20ec
fix: use inputs as ENV vars
cryptodev-2s f20459f
refactor: check labels before running any workflow steps
cryptodev-2s fce53f7
fix: rename changelog check workflow name
cryptodev-2s 5a10b32
Update ref to main instead of testing branch
cryptodev-2s File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| name: Check Changelog | ||
|
|
||
| on: | ||
| workflow_call: | ||
| inputs: | ||
| base-branch: | ||
| required: false | ||
| type: string | ||
| head-ref: | ||
| required: true | ||
| type: string | ||
| labels: | ||
| description: 'JSON string of PR labels' | ||
| required: true | ||
| type: string | ||
| repo: | ||
| description: 'The repository to check' | ||
| required: true | ||
| type: string | ||
| secrets: | ||
| gh-token: | ||
| required: true | ||
|
|
||
| jobs: | ||
| check-changelog: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Check PR Labels | ||
| id: label-check | ||
| env: | ||
| PR_LABELS: ${{ inputs.labels }} | ||
| run: | | ||
| if echo "$PR_LABELS" | jq -e '.[] | select(.name == "no-changelog")' > /dev/null; then | ||
| echo "no-changelog label found, skipping changelog check." | ||
| echo "skip_check=true" >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo "No no-changelog label found, proceeding with check." | ||
| echo "skip_check=false" >> "$GITHUB_OUTPUT" | ||
| fi | ||
| shell: bash | ||
|
|
||
| - name: Check out target repository | ||
| if: ${{ steps.label-check.outputs.skip_check != 'true' }} | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| repository: ${{ inputs.repo }} | ||
| ref: ${{ inputs.head-ref }} | ||
| path: target-repo | ||
| fetch-depth: 0 | ||
|
|
||
| - name: Checkout github-tools repository | ||
| if: ${{ steps.label-check.outputs.skip_check != 'true' }} | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| repository: MetaMask/github-tools | ||
| ref: main | ||
| path: github-tools | ||
|
|
||
| - name: Enable Corepack | ||
| if: ${{ steps.label-check.outputs.skip_check != 'true' }} | ||
| run: corepack enable | ||
| shell: bash | ||
|
|
||
| - name: Set up Node.js | ||
| if: ${{ steps.label-check.outputs.skip_check != 'true' }} | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version-file: ./github-tools/.nvmrc | ||
| cache-dependency-path: ./github-tools/yarn.lock | ||
| cache: yarn | ||
|
|
||
| - name: Install dependencies | ||
| if: ${{ steps.label-check.outputs.skip_check != 'true' }} | ||
| run: yarn --immutable | ||
| shell: bash | ||
| working-directory: ./github-tools | ||
|
|
||
| - name: Check Changelog | ||
| if: ${{ steps.label-check.outputs.skip_check != 'true' }} | ||
| id: changelog-check | ||
| shell: bash | ||
| working-directory: ./github-tools | ||
| env: | ||
| BASE_BRANCH: ${{ inputs.base-branch || 'main' }} | ||
| run: | | ||
| yarn run changelog:check ../target-repo "$BASE_BRANCH" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,259 @@ | ||
| import { parseChangelog } from '@metamask/auto-changelog'; | ||
| import { execa } from 'execa'; | ||
| import fs from 'fs/promises'; | ||
| import path from 'path'; | ||
|
|
||
| type PackageJson = { | ||
| workspaces: string[]; | ||
| }; | ||
|
|
||
| /** | ||
| * Gets the workspace patterns from package.json. | ||
| * | ||
| * @param repoPath - The path to the repository. | ||
| * @returns Array of workspace patterns. | ||
| */ | ||
| async function getWorkspacePatterns(repoPath: string): Promise<string[]> { | ||
| const packageJsonPath = path.join(repoPath, 'package.json'); | ||
| const content = await fs.readFile(packageJsonPath, 'utf-8'); | ||
| const packageJson = JSON.parse(content) as PackageJson; | ||
|
|
||
| if (!Array.isArray(packageJson.workspaces)) { | ||
| return []; | ||
| } | ||
|
|
||
| return packageJson.workspaces; | ||
| } | ||
|
|
||
| /** | ||
| * This function gets the workspace base and package name from the file path. | ||
| * | ||
| * @param filePath - The path to the file. | ||
| * @param workspacePatterns - The workspace patterns. | ||
| * @returns An object containing the base directory and package name, or null if no match is found. | ||
| */ | ||
| function getPackageInfo( | ||
| filePath: string, | ||
| workspacePatterns: string[], | ||
| ): { base: string; package: string } | null { | ||
| for (const pattern of workspacePatterns) { | ||
| // Extract the base directory (everything before the *) | ||
| const wildcardIndex = pattern.indexOf('*'); | ||
| if (wildcardIndex === -1) { | ||
| continue; | ||
| } | ||
|
|
||
| const baseDir = pattern.substring(0, wildcardIndex); | ||
|
|
||
| if (filePath.startsWith(baseDir)) { | ||
| // Extract the package name (everything between baseDir and the next slash) | ||
| const remainingPath = filePath.substring(baseDir.length); | ||
| const nextSlashIndex = remainingPath.indexOf('/'); | ||
|
|
||
| if (nextSlashIndex !== -1) { | ||
| const packageName = remainingPath.substring(0, nextSlashIndex); | ||
| return { | ||
| base: baseDir, | ||
| package: packageName, | ||
| }; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| /** | ||
| * Gets the list of changed files between the current branch and baseRef. | ||
| * | ||
| * @param repoPath - The path to the repository. | ||
| * @param baseRef - The base reference to compare against. | ||
| * @returns Array of changed file paths. | ||
| */ | ||
| async function getChangedFiles( | ||
| repoPath: string, | ||
| baseRef: string, | ||
| ): Promise<string[]> { | ||
| try { | ||
| await execa('git', ['fetch', 'origin', baseRef], { | ||
| cwd: repoPath, | ||
| }); | ||
|
|
||
| const { stdout } = await execa( | ||
| 'git', | ||
| ['diff', '--name-only', `origin/${baseRef}...HEAD`], | ||
| { | ||
| cwd: repoPath, | ||
| }, | ||
| ); | ||
|
|
||
| return stdout.split('\n').filter(Boolean); | ||
| } catch (error) { | ||
| console.error('Failed to get changed files:', error); | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Reads and validates a changelog file. | ||
| * | ||
| * @param changelogPath - The path to the changelog file to check. | ||
| */ | ||
| async function checkChangelogFile(changelogPath: string): Promise<void> { | ||
| try { | ||
| const changelogContent = await fs.readFile(changelogPath, 'utf-8'); | ||
|
|
||
| if (!changelogContent) { | ||
| throw new Error('CHANGELOG.md is empty or missing'); | ||
| } | ||
|
|
||
| const changelogUnreleasedChanges = parseChangelog({ | ||
| changelogContent, | ||
| repoUrl: '', // Not needed as we're only parsing unreleased changes | ||
| }).getReleaseChanges('Unreleased'); | ||
|
|
||
| if (Object.values(changelogUnreleasedChanges).length === 0) { | ||
| throw new Error( | ||
| "❌ No new entries detected under '## Unreleased' section. Please update the changelog.", | ||
| ); | ||
| } | ||
| } catch (error) { | ||
| if (error instanceof Error && 'code' in error && error.code === 'ENOENT') { | ||
| throw new Error(`❌ CHANGELOG.md not found at ${changelogPath}`); | ||
| } | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Gets the list of changed packages from the changed files. | ||
| * | ||
| * @param files - The list of changed files. | ||
| * @param workspacePatterns - The workspace patterns. | ||
| * @returns Array of changed package information. | ||
| */ | ||
| async function getChangedPackages( | ||
| files: string[], | ||
| workspacePatterns: string[], | ||
| ): Promise< | ||
| { | ||
| base: string; | ||
| package: string; | ||
| }[] | ||
| > { | ||
| const changedPackages = new Map<string, { base: string; package: string }>(); | ||
|
|
||
| for (const file of files) { | ||
| // Skip workflow files | ||
| if (file.startsWith('.github/workflows/')) { | ||
| continue; | ||
| } | ||
|
|
||
| const packageInfo = getPackageInfo(file, workspacePatterns); | ||
| if (packageInfo) { | ||
| // Skip test files, docs, and changelog files | ||
| if ( | ||
| !file.match(/\.(test|spec)\./u) && | ||
| !file.includes('__tests__/') && | ||
| !file.includes('/docs/') && | ||
| !file.endsWith('CHANGELOG.md') | ||
| ) { | ||
| changedPackages.set(packageInfo.package, packageInfo); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return Array.from(changedPackages.values()); | ||
| } | ||
|
|
||
| /** | ||
| * Main function to run the changelog check. | ||
| */ | ||
| async function main() { | ||
| // Parse command-line arguments | ||
| const args = process.argv.slice(2); | ||
|
|
||
| const [repoPath, baseRef] = args; | ||
|
|
||
| if (!repoPath || !baseRef) { | ||
| console.error( | ||
| '❌ Usage: ts-node src/check-changelog.ts <repo-path> <base-ref>', | ||
| ); | ||
| throw new Error('❌ Missing required arguments.'); | ||
| } | ||
|
|
||
| const fullRepoPath = path.resolve(process.cwd(), repoPath); | ||
|
|
||
| // Verify the repo path exists | ||
| try { | ||
| await fs.access(fullRepoPath); | ||
| } catch { | ||
| throw new Error(`Repository path not found: ${fullRepoPath}`); | ||
| } | ||
|
|
||
| const workspacePatterns = await getWorkspacePatterns(fullRepoPath); | ||
|
|
||
| if (workspacePatterns.length > 0) { | ||
| console.log( | ||
| 'Running in monorepo mode - checking changelogs for changed packages...', | ||
| ); | ||
|
|
||
| const changedFiles = await getChangedFiles(fullRepoPath, baseRef); | ||
| if (!changedFiles.length) { | ||
| console.log('No changed files found. Exiting successfully.'); | ||
| return; | ||
| } | ||
|
|
||
| const changedPackages = await getChangedPackages( | ||
| changedFiles, | ||
| workspacePatterns, | ||
| ); | ||
| if (!changedPackages.length) { | ||
| console.log( | ||
| 'No package code changes detected that would require changelog updates.', | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| const checkResults = await Promise.all( | ||
| changedPackages.map(async (pkgInfo) => { | ||
| try { | ||
| await checkChangelogFile( | ||
| path.join( | ||
| fullRepoPath, | ||
| pkgInfo.base, | ||
| pkgInfo.package, | ||
| 'CHANGELOG.md', | ||
| ), | ||
| ); | ||
| console.log( | ||
| `✅ CHANGELOG.md for ${pkgInfo.package} has been correctly updated.`, | ||
| ); | ||
| return { package: pkgInfo.package, success: true }; | ||
| } catch (error) { | ||
| console.error( | ||
| `❌ Changelog check failed for package ${pkgInfo.package}:`, | ||
| error, | ||
| ); | ||
| return { package: pkgInfo.package, success: false, error }; | ||
| } | ||
| }), | ||
| ); | ||
|
|
||
| const hasError = checkResults.some((result) => !result.success); | ||
|
|
||
| if (hasError) { | ||
| throw new Error('One or more changelog checks failed'); | ||
| } | ||
| } else { | ||
| console.log( | ||
| 'Running in single-repo mode - checking changelog for the entire repository...', | ||
| ); | ||
| await checkChangelogFile(path.join(fullRepoPath, 'CHANGELOG.md')); | ||
| console.log('✅ CHANGELOG.md has been correctly updated.'); | ||
| } | ||
| } | ||
|
|
||
| main().catch((error) => { | ||
| throw error; | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It’s not related to the newly added workflow, but the lint workflow was failing.