| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You 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. |
| */ |
| |
| import { execSync } from 'child_process'; |
| import { readFileSync } from 'fs'; |
| import { join } from 'path'; |
| |
| // Types |
| interface Version { |
| tag: string; |
| ref: string; |
| } |
| |
| interface PR { |
| number: number; |
| title: string; |
| commit: string; |
| } |
| |
| // Configuration |
| const IGNORE_TYPES = [ |
| 'docs', |
| 'chore', |
| 'test', |
| 'ci' |
| ]; |
| |
| const IGNORE_PRS = [ |
| // 3.9.0 |
| 10655, 10857, 10858, 10887, 10959, 11029, 11041, 11053, 11055, 11061, 10976, 10984, 11025, |
| // 3.10.0 |
| 11105, 11128, 11169, 11171, 11280, 11333, 11081, 11202, 11469, |
| // 3.11.0 |
| 11463, 11570, |
| // 3.12.0 |
| 11769, 11816, 11881, 11905, 11924, 11926, 11973, 11991, 11992, 11829, |
| // 3.13.0 |
| 9945, 11420, 11765, 12036, 12048, 12057, 12076, 12122, 12123, 12168, 12199, 12218, 12225, 12272, 12277, 12300, 12306, 12329, 12353, 12364, 12375, 12358 |
| ]; |
| |
| |
| function getGitRef(version: string): string { |
| try { |
| execSync(`git rev-parse ${version}`, { stdio: 'ignore' }); |
| return version; |
| } catch { |
| return 'HEAD'; |
| } |
| } |
| |
| function extractVersionsFromChangelog(): Version[] { |
| const changelogPath = join(process.cwd(), '..', 'CHANGELOG.md'); |
| const content = readFileSync(changelogPath, 'utf-8'); |
| const versionRegex = /^## ([0-9]+\.[0-9]+\.[0-9]+)/gm; |
| const versions: Version[] = []; |
| let match; |
| |
| while ((match = versionRegex.exec(content)) !== null) { |
| const tag = match[1]; |
| versions.push({ |
| tag, |
| ref: getGitRef(tag) |
| }); |
| } |
| |
| return versions; |
| } |
| |
| function extractPRsFromChangelog(startTag: string, endTag: string): number[] { |
| const changelogPath = join(process.cwd(), '..', 'CHANGELOG.md'); |
| const content = readFileSync(changelogPath, 'utf-8'); |
| const lines = content.split('\n'); |
| let inRange = false; |
| const prs: number[] = []; |
| |
| for (const line of lines) { |
| if (line.startsWith(`## ${startTag}`)) { |
| inRange = true; |
| continue; |
| } |
| if (inRange && line.startsWith(`## ${endTag}`)) { |
| break; |
| } |
| if (inRange) { |
| const match = line.match(/#(\d+)/); |
| if (match) { |
| prs.push(parseInt(match[1], 10)); |
| } |
| } |
| } |
| |
| return prs.sort((a, b) => a - b); |
| } |
| |
| function shouldIgnoreCommitMessage(message: string): boolean { |
| // Extract the commit message part (remove the commit hash) |
| const messagePart = message.split(' ').slice(1).join(' '); |
| |
| // Check if the message starts with any of the ignored types |
| for (const type of IGNORE_TYPES) { |
| // Check simple format: "type: message" |
| if (messagePart.startsWith(`${type}:`)) { |
| return true; |
| } |
| // Check format with scope: "type(scope): message" |
| if (messagePart.startsWith(`${type}(`)) { |
| const closingBracketIndex = messagePart.indexOf('):'); |
| if (closingBracketIndex !== -1) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| function extractPRsFromGitLog(oldRef: string, newRef: string): PR[] { |
| const log = execSync(`git log ${oldRef}..${newRef} --oneline`, { encoding: 'utf-8' }); |
| const prs: PR[] = []; |
| |
| for (const line of log.split('\n')) { |
| if (!line.trim()) continue; |
| |
| // Check if this commit should be ignored |
| if (shouldIgnoreCommitMessage(line)) continue; |
| |
| // Find PR number |
| const prMatch = line.match(/#(\d+)/); |
| if (prMatch) { |
| const prNumber = parseInt(prMatch[1], 10); |
| if (!IGNORE_PRS.includes(prNumber)) { |
| prs.push({ |
| number: prNumber, |
| title: line, |
| commit: line.split(' ')[0] |
| }); |
| } |
| } |
| } |
| |
| return prs.sort((a, b) => a.number - b.number); |
| } |
| |
| function findMissingPRs(changelogPRs: number[], gitPRs: PR[]): PR[] { |
| const changelogPRSet = new Set(changelogPRs); |
| return gitPRs.filter(pr => !changelogPRSet.has(pr.number)); |
| } |
| |
| function versionGreaterThan(v1: string, v2: string): boolean { |
| // Remove 'v' prefix if present |
| const cleanV1 = v1.replace(/^v/, ''); |
| const cleanV2 = v2.replace(/^v/, ''); |
| |
| // Split version strings into arrays of numbers |
| const v1Parts = cleanV1.split('.').map(Number); |
| const v2Parts = cleanV2.split('.').map(Number); |
| |
| // Compare each part |
| for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) { |
| const v1Part = v1Parts[i] || 0; |
| const v2Part = v2Parts[i] || 0; |
| |
| if (v1Part > v2Part) return true; |
| if (v1Part < v2Part) return false; |
| } |
| |
| // If all parts are equal, return false |
| return false; |
| } |
| |
| // Main function |
| async function main() { |
| try { |
| const versions = extractVersionsFromChangelog(); |
| let hasErrors = false; |
| |
| for (let i = 0; i < versions.length - 1; i++) { |
| const newVersion = versions[i]; |
| const oldVersion = versions[i + 1]; |
| |
| // Skip if new version is less than or equal to 3.8.0 |
| if (!versionGreaterThan(newVersion.tag, '3.8.0')) { |
| continue; |
| } |
| |
| console.log(`\n=== Checking changes between ${newVersion.tag} (${newVersion.ref}) and ${oldVersion.tag} (${oldVersion.ref}) ===`); |
| |
| const changelogPRs = extractPRsFromChangelog(newVersion.tag, oldVersion.tag); |
| const gitPRs = extractPRsFromGitLog(oldVersion.ref, newVersion.ref); |
| const missingPRs = findMissingPRs(changelogPRs, gitPRs); |
| |
| console.log(`\n=== PR Comparison Results for ${newVersion.tag} ===`); |
| |
| if (missingPRs.length === 0) { |
| console.log(`\n✅ All PRs are included in CHANGELOG.md for version ${newVersion.tag}`); |
| } else { |
| console.log(`\n❌ Missing PRs in CHANGELOG.md for version ${newVersion.tag} (sorted):`); |
| missingPRs.forEach(pr => { |
| console.log(` #${pr.number}`); |
| }); |
| |
| console.log(`\nDetailed information about missing PRs for version ${newVersion.tag}:`); |
| missingPRs.forEach(pr => { |
| console.log(`\nPR #${pr.number}:`); |
| console.log(` - ${pr.title}`); |
| console.log(` - PR URL: https://github.com/apache/apisix/pull/${pr.number}`); |
| }); |
| |
| console.log('Note: If you confirm that a PR should not appear in the changelog, please add its number to the IGNORE_PRS array in this script.'); |
| hasErrors = true; |
| } |
| } |
| |
| if (hasErrors) { |
| process.exit(1); |
| } |
| } catch (error) { |
| console.error('Error:', error); |
| process.exit(1); |
| } |
| } |
| |
| (async () => { |
| await main(); |
| })(); |