Skip to content

Commit f7d3f7c

Browse files
committed
chore: GH action to remind developers to add release notes in CHANGELOG.md
1 parent 67c3620 commit f7d3f7c

File tree

5 files changed

+352
-0
lines changed

5 files changed

+352
-0
lines changed

.github/workflows/changelog-check.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: ChangeLog Check
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, labeled, unlabeled]
6+
merge_group:
7+
types: [checks_requested]
8+
9+
jobs:
10+
check_changelog:
11+
uses: './.github/workflows/shared-changelog-check.yml'
12+
with:
13+
event-type: ${{ github.event_name }}
14+
pr-number: ${{ github.event.pull_request.number }}
15+
feature-branch: ${{ github.head_ref }}
16+
is-monorepo: true
17+
secrets:
18+
gh-token: ${{ secrets.GITHUB_TOKEN }}
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
name: Shared ChangeLog Check
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
feature-branch:
7+
required: true
8+
type: string
9+
pr-number:
10+
required: false
11+
type: string
12+
event-type:
13+
required: true
14+
type: string
15+
is-monorepo:
16+
description: 'Set to true for monorepo projects (only yarn workspaces are supported)'
17+
required: false
18+
type: boolean
19+
default: false
20+
secrets:
21+
gh-token:
22+
required: true
23+
24+
jobs:
25+
shared_changelog_check:
26+
runs-on: ubuntu-latest
27+
steps:
28+
- name: Conditional Skip or Execute
29+
run: |
30+
if [[ "${{ inputs.event-type }}" == "merge_group" ]]; then
31+
echo "Merge group event detected, auto-succeeding."
32+
exit 0
33+
fi
34+
echo "Proceeding with pull request checks."
35+
36+
- name: Checkout github-tools repository
37+
if: ${{ inputs.event-type == 'pull_request' }}
38+
uses: actions/checkout@v4
39+
with:
40+
repository: MetaMask/core
41+
ref: changelog-checker
42+
path: github-tools
43+
44+
- name: Enable Corepack
45+
if: ${{ inputs.event-type == 'pull_request' }}
46+
run: corepack enable
47+
shell: bash
48+
49+
- name: Set up Node.js
50+
if: ${{ inputs.event-type == 'pull_request' }}
51+
uses: actions/setup-node@v4
52+
with:
53+
node-version-file: ./github-tools/.nvmrc
54+
cache-dependency-path: ./github-tools/yarn.lock
55+
cache: yarn
56+
57+
- name: Install dependencies
58+
if: ${{ inputs.event-type == 'pull_request' }}
59+
run: yarn --immutable
60+
shell: bash
61+
working-directory: ./github-tools
62+
63+
- name: Check PR Labels
64+
if: ${{ inputs.event-type == 'pull_request' }}
65+
id: label-check
66+
env:
67+
GITHUB_TOKEN: ${{ secrets.gh-token }}
68+
GITHUB_REPO: ${{ github.repository }}
69+
PR_NUMBER: ${{ inputs.pr-number }}
70+
run: |
71+
# Fetch labels from the GitHub API
72+
if ! labels=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \
73+
"https://api.github.com/repos/${GITHUB_REPO}/issues/${PR_NUMBER}/labels"); then
74+
echo "::error::Failed to fetch labels from GitHub API."
75+
exit 1
76+
fi
77+
78+
# Proceed with checking for the 'no-changelog' label using jq
79+
if echo "$labels" | jq -e '.[] | select(.name == "no-changelog")'; then
80+
echo "No-changelog label found, skipping changelog check."
81+
echo "SKIP_CHANGELOG=true" >> "$GITHUB_ENV"
82+
else
83+
echo "SKIP_CHANGELOG=false" >> "$GITHUB_ENV"
84+
echo "No-changelog label not found, proceeding with changelog check."
85+
fi
86+
shell: bash
87+
88+
- name: Check Changelog
89+
if: ${{ inputs.event-type == 'pull_request' && env.SKIP_CHANGELOG != 'true' }}
90+
id: changelog-check
91+
shell: bash
92+
env:
93+
GITHUB_TOKEN: ${{ secrets.gh-token }}
94+
IS_MONOREPO: ${{ inputs.is-monorepo || 'false' }}
95+
GITHUB_REPO: ${{ github.repository }}
96+
HEAD_REF: ${{ github.head_ref }}
97+
PR_NUMBER: ${{ inputs.pr-number }}
98+
working-directory: ./github-tools
99+
run: |
100+
if [[ "$IS_MONOREPO" == "true" ]]; then
101+
echo "Running in monorepo mode - checking changelogs for changed packages..."
102+
103+
# Get all changed files with pagination support
104+
CHANGED_FILES=""
105+
PAGE=1
106+
while true; do
107+
echo "Fetching changed files (page $PAGE)..."
108+
109+
if ! PAGE_RESPONSE=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \
110+
"https://api.github.com/repos/${GITHUB_REPO}/pulls/${PR_NUMBER}/files?page=$PAGE&per_page=100"); then
111+
echo "::error::Failed to fetch changed files from GitHub API."
112+
exit 1
113+
fi
114+
115+
# Check if the response is a valid JSON array
116+
if ! echo "$PAGE_RESPONSE" | jq -e 'if type=="array" then true else false end' > /dev/null; then
117+
echo "::error::Invalid response from GitHub API."
118+
echo "Response:"
119+
echo "$PAGE_RESPONSE" | jq '.'
120+
exit 1
121+
fi
122+
123+
PAGE_FILES=$(echo "$PAGE_RESPONSE" | jq -r '.[].filename')
124+
ITEMS_COUNT=$(echo "$PAGE_RESPONSE" | jq 'length')
125+
126+
# If no files returned or empty array, we've reached the end of pagination
127+
if [ -z "$PAGE_FILES" ] || [ "$ITEMS_COUNT" == "0" ]; then
128+
break
129+
fi
130+
131+
# Append files from this page
132+
if [ -n "$CHANGED_FILES" ]; then
133+
CHANGED_FILES="$CHANGED_FILES"$'\n'"$PAGE_FILES"
134+
else
135+
CHANGED_FILES="$PAGE_FILES"
136+
fi
137+
138+
# Break if returned items fewer than per_page (indicating last page)
139+
if [ "$ITEMS_COUNT" -lt 100 ]; then
140+
break
141+
fi
142+
143+
# Go to next page
144+
PAGE=$((PAGE+1))
145+
done
146+
147+
# Clean up empty lines
148+
CHANGED_FILES=$(echo "$CHANGED_FILES" | grep -v '^$' || echo "")
149+
150+
# Check if we got any files after pagination and cleanup - exit early if there are no changed files to process
151+
if [ -z "$CHANGED_FILES" ]; then
152+
echo "No changed files found. Exiting successfully."
153+
exit 0
154+
fi
155+
156+
# Debug output
157+
echo "Files changed in this PR:"
158+
echo "$CHANGED_FILES"
159+
echo "Total: $(echo "$CHANGED_FILES" | wc -l) files"
160+
161+
# Identify changed packages
162+
declare -a CHANGED_PACKAGES
163+
164+
# Extract package names from file paths
165+
while IFS= read -r FILE; do
166+
if [[ "$FILE" =~ ^packages/([^/]+)/ ]]; then
167+
PACKAGE="${BASH_REMATCH[1]}"
168+
169+
# Skip test files, docs, yaml configs, and changelog files
170+
if [[ ! "$FILE" =~ \.(test|spec)\. ]] && \
171+
[[ ! "$FILE" =~ __tests__/ ]] && \
172+
[[ ! "$FILE" =~ ^packages/$PACKAGE/docs/ ]] && \
173+
[[ ! "$FILE" =~ \.(ya?ml)$ ]] && \
174+
[[ ! "$FILE" =~ ^packages/$PACKAGE/CHANGELOG\.md$ ]]; then
175+
176+
# Add package to our tracking array if not already there
177+
if [[ ! " ${CHANGED_PACKAGES[*]} " =~ \ ${PACKAGE}\ ]]; then
178+
CHANGED_PACKAGES+=("$PACKAGE")
179+
echo "Code changes detected in package: $PACKAGE"
180+
fi
181+
fi
182+
fi
183+
done <<< "$CHANGED_FILES"
184+
185+
# Skip if no packages with code changes were found
186+
if [ ${#CHANGED_PACKAGES[@]} -eq 0 ]; then
187+
echo "No package code changes detected that would require changelog updates."
188+
exit 0
189+
fi
190+
191+
# Check changelogs for each changed package
192+
OVERALL_STATUS=0
193+
for PACKAGE in "${CHANGED_PACKAGES[@]}"; do
194+
echo "Checking changelog for package: $PACKAGE"
195+
CHANGELOG_PATH="packages/${PACKAGE}/CHANGELOG.md"
196+
197+
# Call the existing changelog:check script for each package
198+
if ! yarn run changelog:check "$GITHUB_REPO" "$HEAD_REF" "$CHANGELOG_PATH"; then
199+
OVERALL_STATUS=1
200+
echo "::error::Changelog check failed for package: $PACKAGE"
201+
fi
202+
done
203+
204+
# Exit with the overall status
205+
exit $OVERALL_STATUS
206+
else
207+
echo "Running in single-repo mode - checking changelog for the entire repository..."
208+
# Original single-repo check
209+
yarn run changelog:check "$GITHUB_REPO" "$HEAD_REF" "CHANGELOG.md"
210+
fi

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"build:only-clean": "rimraf -g 'packages/*/dist'",
1919
"build:docs": "yarn workspaces foreach --all --no-private --parallel --interlaced --verbose run build:docs",
2020
"build:types": "tsc --build tsconfig.build.json --verbose",
21+
"changelog:check": "ts-node scripts/changelog-check.ts",
2122
"changelog:update": "yarn workspaces foreach --all --no-private --parallel --interlaced --verbose run changelog:update",
2223
"changelog:validate": "yarn workspaces foreach --all --no-private --parallel --interlaced --verbose run changelog:validate",
2324
"create-package": "ts-node scripts/create-package",
@@ -54,6 +55,7 @@
5455
"@babel/preset-typescript": "^7.23.3",
5556
"@lavamoat/allow-scripts": "^3.0.4",
5657
"@lavamoat/preinstall-always-fail": "^2.1.0",
58+
"@metamask/auto-changelog": "^3.4.4",
5759
"@metamask/create-release-branch": "^4.1.1",
5860
"@metamask/eslint-config": "^14.0.0",
5961
"@metamask/eslint-config-jest": "^14.0.0",

scripts/changelog-check.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { parseChangelog } from '@metamask/auto-changelog';
2+
3+
/**
4+
* Asynchronously fetches the CHANGELOG.md file content from a specified GitHub repository and branch.
5+
* The function constructs a URL to access the raw content of the file using GitHub's raw content service.
6+
* It handles authorization using an optional GitHub token from environment variables.
7+
*
8+
* @param options - The options for fetching the CHANGELOG.md file.
9+
* @param options.repoUrl - The full name of the repository (e.g., "owner/repo").
10+
* @param options.changelogPath - The path to the CHANGELOG.md file.
11+
* @param options.branch - The branch from which to fetch the CHANGELOG.md file.
12+
* @returns A promise that resolves to the content of the CHANGELOG.md file as a string.
13+
* If the fetch operation fails, it logs an error and returns an empty string.
14+
*/
15+
async function fetchChangelogFromGitHub({
16+
repoUrl,
17+
changelogPath,
18+
branch,
19+
}: {
20+
repoUrl: string;
21+
changelogPath: string;
22+
branch: string;
23+
}): Promise<string> {
24+
try {
25+
const url = `https://raw.githubusercontent.com/${repoUrl}/${branch}/${changelogPath}`;
26+
// eslint-disable-next-line n/no-process-env
27+
const token = process.env.GITHUB_TOKEN ?? '';
28+
const headers: HeadersInit = token
29+
? { Authorization: `Bearer ${token}` }
30+
: {};
31+
// eslint-disable-next-line n/no-unsupported-features/node-builtins -- This script only runs in CI with Node.js >=18
32+
const response = await fetch(url, { headers });
33+
34+
if (!response.ok) {
35+
throw new Error(`HTTP error! Status: ${response.status}`);
36+
}
37+
38+
return await response.text();
39+
} catch (error) {
40+
console.error(
41+
`❌ Error fetching CHANGELOG.md from ${branch} on ${repoUrl}:`,
42+
error,
43+
);
44+
throw error;
45+
}
46+
}
47+
48+
/**
49+
* Validates that the CHANGELOG.md in a feature branch has been updated correctly by comparing it
50+
* against the CHANGELOG.md in the base branch.
51+
*
52+
* @param options - The options for the changelog check.
53+
* @param options.repoUrl - The GitHub repository from which to fetch the CHANGELOG.md file.
54+
* @param options.changelogPath - The path to the CHANGELOG.md file.
55+
* @param options.featureBranch - The feature branch that should contain the updated CHANGELOG.md.
56+
*/
57+
async function checkChangelog({
58+
repoUrl,
59+
changelogPath,
60+
featureBranch,
61+
}: {
62+
repoUrl: string;
63+
changelogPath: string;
64+
featureBranch: string;
65+
}) {
66+
console.log(
67+
`🔍 Fetching CHANGELOG.md from GitHub repository: ${repoUrl} ${changelogPath}`,
68+
);
69+
70+
const changelogContent = await fetchChangelogFromGitHub({
71+
repoUrl,
72+
changelogPath,
73+
branch: featureBranch,
74+
});
75+
76+
if (!changelogContent) {
77+
console.error('❌ CHANGELOG.md is missing in the feature branch.');
78+
throw new Error('❌ CHANGELOG.md is missing in the feature branch.');
79+
}
80+
81+
const changelogUnreleasedChanges = parseChangelog({
82+
changelogContent,
83+
repoUrl,
84+
}).getReleaseChanges('Unreleased');
85+
86+
if (Object.values(changelogUnreleasedChanges).length === 0) {
87+
throw new Error(
88+
"❌ No new entries detected under '## Unreleased'. Please update the changelog.",
89+
);
90+
}
91+
92+
console.log('✅ CHANGELOG.md has been correctly updated.');
93+
}
94+
95+
// Parse command-line arguments
96+
const args = process.argv.slice(2);
97+
if (args.length < 3) {
98+
console.error(
99+
'❌ Usage: ts-node src/check-changelog.ts <repo-url> <feature-branch> <changelog-path>',
100+
);
101+
throw new Error('❌ Missing required arguments.');
102+
}
103+
104+
const [repoUrl, featureBranch, changelogPath] = args;
105+
106+
// Ensure all required arguments are provided
107+
if (!repoUrl || !featureBranch || !changelogPath) {
108+
console.error(
109+
'❌ Usage: ts-node src/check-changelog.ts <repo-url> <feature-branch> <changelog-path>',
110+
);
111+
throw new Error('❌ Missing required arguments.');
112+
}
113+
114+
// Run the validation
115+
checkChangelog({
116+
repoUrl,
117+
changelogPath,
118+
featureBranch,
119+
}).catch((error) => {
120+
throw error;
121+
});

yarn.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2875,6 +2875,7 @@ __metadata:
28752875
"@babel/preset-typescript": "npm:^7.23.3"
28762876
"@lavamoat/allow-scripts": "npm:^3.0.4"
28772877
"@lavamoat/preinstall-always-fail": "npm:^2.1.0"
2878+
"@metamask/auto-changelog": "npm:^3.4.4"
28782879
"@metamask/create-release-branch": "npm:^4.1.1"
28792880
"@metamask/eslint-config": "npm:^14.0.0"
28802881
"@metamask/eslint-config-jest": "npm:^14.0.0"

0 commit comments

Comments
 (0)