Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/actionlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ paths:
ignore:
# We need to ignore the expected missing inputs in test-checkout-and-setup.yml
- 'missing input "is-high-risk-environment".+'
.github/workflows/changelog-check.yml:
ignore:
# Ignore SC2086 warnings related to shell commands
- 'SC2086:.+'
Comment thread
jake-perkins marked this conversation as resolved.
74 changes: 74 additions & 0 deletions .github/workflows/changelog-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
name: Changelog Check

on:
workflow_call:
inputs:
base-branch:
required: true
type: string
feature-branch:
required: true
type: string
pr-number:
required: false
type: string
secrets:
gh-token:
required: true

jobs:
check-changelog:
runs-on: ubuntu-latest
steps:
- name: Checkout github-tools repository
uses: actions/checkout@v4
with:
repository: MetaMask/github-tools
ref: changelog-checker
path: github-tools
Comment thread
jake-perkins marked this conversation as resolved.

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version-file: ./github-tools/.nvmrc
cache-dependency-path: ./github-tools/yarn.lock
cache: yarn

- name: Enable Corepack
run: corepack enable
shell: bash
working-directory: ./github-tools

- name: Install dependencies
run: yarn --immutable
shell: bash
working-directory: ./github-tools
Comment thread
jake-perkins marked this conversation as resolved.

- name: Check PR Labels
id: label-check
run: |
# Fetch labels from the GitHub API
labels=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/${{ github.repository }}/issues/${{ inputs.pr-number }}/labels")

# Proceed with checking for the 'no-changelog' label using jq
if echo "$labels" | jq -e '.[] | select(.name == "no-changelog")'; then
echo "No-changelog label found, skipping changelog check."
echo "SKIP_CHANGELOG=true" >> $GITHUB_ENV
else
echo "SKIP_CHANGELOG=false" >> $GITHUB_ENV
echo "No-changelog label not found, proceeding with changelog check."
fi
env:
GITHUB_TOKEN: ${{ secrets.gh-token }}
shell: bash

- name: Check Changelog
if: env.SKIP_CHANGELOG != 'true'
id: changelog-check
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.gh-token }}
working-directory: ./github-tools
run: |
yarn run changelog-check ${{ github.repository }} ${{ inputs.base-branch }} ${{ inputs.feature-branch }}
18 changes: 14 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"private": true,
"description": "Tools for interacting with the GitHub API to do metrics gathering",
"scripts": {
"changelog-check": "ts-node src/changelog-check.ts",
"count-references-to-contributor-docs": "ts-node --swc src/scripts/count-references-to-contributor-docs/cli.ts",
"gen:commits": "node .github/scripts/generate-rc-commits.mjs",
"get-review-metrics": "ts-node src/get-review-metrics.ts",
Expand All @@ -20,6 +21,7 @@
"update-release-sheet": "node .github/scripts/update-release-sheet.mjs"
},
"dependencies": {
"@metamask/auto-changelog": "4.1.0",
"@metamask/utils": "^7.1.0",
"@octokit/graphql": "^7.0.1",
"@octokit/request": "^8.1.1",
Expand All @@ -30,6 +32,7 @@
"csv-parse": "5.6.0",
"googleapis": "144.0.0",
"luxon": "^3.3.0",
"node-fetch": "2.6.12",
"ora": "^5.4.1",
"simple-git": "3.27.0"
},
Expand All @@ -42,23 +45,27 @@
"@metamask/eslint-config-typescript": "^12.0.0",
"@swc/cli": "^0.1.62",
"@swc/core": "^1.3.80",
"@types/axios": "^0.14.4",
"@types/diff": "^7",
"@types/fs-extra": "^11.0.4",
"@types/jest": "^28.1.6",
"@types/node": "^20.3.2",
"@types/node-fetch": "^2.6.12",
"@typescript-eslint/eslint-plugin": "^5.43.0",
"@typescript-eslint/parser": "^5.43.0",
"depcheck": "^1.4.3",
"eslint": "^8.44.0",
"eslint-config-prettier": "^8.8.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-jest": "^27.2.2",
"eslint-plugin-jsdoc": "^39.9.1",
"eslint-plugin-n": "^15.7.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-promise": "^6.1.1",
"jest": "^28.1.3",
"jest-it-up": "^2.0.2",
"prettier": "^2.7.1",
"prettier-plugin-packagejson": "^2.3.0",
"prettier": "^3.3.3",
"prettier-plugin-packagejson": "^2.5.2",
"ts-jest": "^28.0.7",
"ts-node": "^10.9.1",
"typescript": "^5.1.3"
Expand All @@ -67,6 +74,9 @@
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"prettier": ">=3.0.0"
},
"lavamoat": {
"allowScripts": {
"@lavamoat/preinstall-always-fail": false,
Expand Down
154 changes: 154 additions & 0 deletions src/changelog-check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { parseChangelog } from '@metamask/auto-changelog';
import nodeFetch from 'node-fetch';

/**
* Asynchronously fetches the CHANGELOG.md file content from a specified GitHub repository and branch.
* The function constructs a URL to access the raw content of the file using GitHub's raw content service.
* It handles authorization using an optional GitHub token from environment variables.
*
* @param repo - The full name of the repository (e.g., "owner/repo").
* @param branch - The branch from which to fetch the CHANGELOG.md file.
* @returns A promise that resolves to the content of the CHANGELOG.md file as a string.
* If the fetch operation fails, it logs an error and returns an empty string.
*/
async function fetchChangelogFromGitHub(
repo: string,
branch: string,
): Promise<string> {
const url = `https://raw.githubusercontent.com/${repo}/${branch}/CHANGELOG.md`;
// eslint-disable-next-line n/no-process-env
const token = process.env.GITHUB_TOKEN ?? '';

try {
const headers = token ? { Authorization: `Bearer ${token}` } : {};
const response = await nodeFetch(url, {
headers,
});

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

return await response.text();
} catch (error) {
console.error(
`❌ Error fetching CHANGELOG.md from ${branch} on ${repo}:`,
error,
);
throw error;
}
}

/**
* Compares the changelog entries between the base and feature branches to determine if there are differences.
*
* @param baseChanges - The content of the '[Unreleased]' section from the base branch's CHANGELOG.md.
* @param featureChanges - The content of the '[Unreleased]' section from the feature branch's CHANGELOG.md.
* @returns Returns true if there are new or differing number of entries in the feature branch compared to the base branch.
*/
function compareChangeLogs(
baseChanges: string[],
featureChanges: string[],
): boolean {
const newEntries = featureChanges.filter(
(entry) => !baseChanges.includes(entry),
);

// Log and return true if there are new entries
if (newEntries.length > 0) {
console.log('New entries in feature branch:', newEntries);
return true;
}

// Check if the number of entries has changed
if (baseChanges.length !== featureChanges.length) {
console.log(
'The number of entries has changed. Base branch has',
baseChanges.length,
'entries, while feature branch has',
featureChanges.length,
'entries.',
);
return true;
}

// If no new entries and the size has not changed, return false
return false;
}

/**
* Validates that the CHANGELOG.md in a feature branch has been updated correctly by comparing it
* against the CHANGELOG.md in the base branch.
* @param repo - The GitHub repository from which to fetch the CHANGELOG.md file.
* @param baseBranch - The base branch (typically 'main' or 'master') to compare against.
* @param featureBranch - The feature branch that should contain the updated CHANGELOG.md.
*/
async function validateChangelog(
repo: string,
baseBranch: string,
featureBranch: string,
) {
console.log(`🔍 Fetching CHANGELOG.md from GitHub repository: ${repo}`);

const [baseChangelogContent, featureChangelogContent] = await Promise.all([
fetchChangelogFromGitHub(repo, baseBranch),
fetchChangelogFromGitHub(repo, featureBranch),
]);

if (!featureChangelogContent) {
throw new Error('❌ CHANGELOG.md is missing in the feature branch.');
Comment thread
jake-perkins marked this conversation as resolved.
}

const baseUnreleasedChanges = parseChangelog({
changelogContent: baseChangelogContent,
repoUrl: '', // Not needed as we're only parsing unreleased changes
}).getReleaseChanges('Unreleased');

const featureUnreleasedChanges = parseChangelog({
changelogContent: featureChangelogContent,
repoUrl: '', // Not needed as we're only parsing unreleased changes
}).getReleaseChanges('Unreleased');

const baseChanges = Object.values(baseUnreleasedChanges).flat();
const featureChanges = Object.values(featureUnreleasedChanges).flat();

console.log('🔍 Comparing changelog entries...');

console.log('Base unreleased section:', baseUnreleasedChanges);
console.log('Feature unreleased section:', featureUnreleasedChanges);

const hasChanges = compareChangeLogs(baseChanges, featureChanges);

if (!hasChanges) {
throw new Error(
"❌ No new entries detected under '## Unreleased'. Please update the changelog.",
);
}

console.log('✅ CHANGELOG.md has been correctly updated.');
}

// Parse command-line arguments
const args = process.argv.slice(2);
if (args.length < 3) {
console.error(
'❌ Usage: ts-node scripts/check-changelog.js <github-repo> <base-branch> <feature-branch>',
);
throw new Error('❌ Missing required arguments.');
Comment thread
jake-perkins marked this conversation as resolved.
}

const [githubRepo, baseBranch, featureBranch] = args;

// Ensure all required arguments are provided
if (!githubRepo || !baseBranch || !featureBranch) {
console.error(
'❌ Usage: ts-node src/check-changelog.ts <github-repo> <base-branch> <feature-branch>',
);
throw new Error('❌ Missing required arguments.');
}

// Run the validation
validateChangelog(githubRepo, baseBranch, featureBranch).catch((error) => {
console.error('❌ Unexpected error:', error);
throw error;
});
2 changes: 1 addition & 1 deletion src/scripts/count-references-to-contributor-docs/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const REPOSITORY_NAMES = [
'snaps',
] as const;

type RepositoryName = typeof REPOSITORY_NAMES[number];
type RepositoryName = (typeof REPOSITORY_NAMES)[number];

/**
* It is not necessary for us to query all of the pull requests or pull requests
Expand Down
Loading