This is an automated email from the ASF dual-hosted git repository.

sunyi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix.git


The following commit(s) were added to refs/heads/master by this push:
     new 1c3cb94f7 ci: add GitHub Action to check CHANGELOG consistency with 
PRs (#12347)
1c3cb94f7 is described below

commit 1c3cb94f75a84d53d0072fc90f3503587da25b42
Author: litesun <su...@apache.org>
AuthorDate: Fri Jun 20 11:39:39 2025 +0800

    ci: add GitHub Action to check CHANGELOG consistency with PRs (#12347)
---
 .github/workflows/check-changelog.yml |  27 ++++
 ci/check_changelog_prs.ts             | 236 ++++++++++++++++++++++++++++++++++
 2 files changed, 263 insertions(+)

diff --git a/.github/workflows/check-changelog.yml 
b/.github/workflows/check-changelog.yml
new file mode 100644
index 000000000..0efef10c1
--- /dev/null
+++ b/.github/workflows/check-changelog.yml
@@ -0,0 +1,27 @@
+name: Check Changelog
+
+on:
+  push:
+    paths:
+      - 'CHANGELOG.md'
+      - 'ci/check_changelog_prs.ts'
+  pull_request:
+    paths:
+      - 'CHANGELOG.md'
+      - 'ci/check_changelog_prs.ts'
+
+jobs:
+  check-changelog:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+
+
+      - name: Run check_changelog_prs script
+        working-directory: ci
+        run: |
+          curl -fsSL https://bun.sh/install | bash
+          export PATH="$HOME/.bun/bin:$PATH"
+          bun run check_changelog_prs.ts
diff --git a/ci/check_changelog_prs.ts b/ci/check_changelog_prs.ts
new file mode 100755
index 000000000..9a0d1ed36
--- /dev/null
+++ b/ci/check_changelog_prs.ts
@@ -0,0 +1,236 @@
+/*
+ * 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
+];
+
+
+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();
+})();

Reply via email to