mirror of
https://github.com/esphome/esphome.git
synced 2026-05-28 13:37:24 +08:00
[ci] Add PR title format check (#14345)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,5 +1,12 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const { DOCS_PR_PATTERNS } = require('./constants');
|
const { DOCS_PR_PATTERNS } = require('./constants');
|
||||||
|
const {
|
||||||
|
COMPONENT_REGEX,
|
||||||
|
detectComponents,
|
||||||
|
hasCoreChanges,
|
||||||
|
hasDashboardChanges,
|
||||||
|
hasGitHubActionsChanges,
|
||||||
|
} = require('../detect-tags');
|
||||||
|
|
||||||
// Strategy: Merge branch detection
|
// Strategy: Merge branch detection
|
||||||
async function detectMergeBranch(context) {
|
async function detectMergeBranch(context) {
|
||||||
@@ -20,15 +27,13 @@ async function detectMergeBranch(context) {
|
|||||||
// Strategy: Component and platform labeling
|
// Strategy: Component and platform labeling
|
||||||
async function detectComponentPlatforms(changedFiles, apiData) {
|
async function detectComponentPlatforms(changedFiles, apiData) {
|
||||||
const labels = new Set();
|
const labels = new Set();
|
||||||
const componentRegex = /^esphome\/components\/([^\/]+)\//;
|
|
||||||
const targetPlatformRegex = new RegExp(`^esphome\/components\/(${apiData.targetPlatforms.join('|')})/`);
|
const targetPlatformRegex = new RegExp(`^esphome\/components\/(${apiData.targetPlatforms.join('|')})/`);
|
||||||
|
|
||||||
for (const file of changedFiles) {
|
for (const comp of detectComponents(changedFiles)) {
|
||||||
const componentMatch = file.match(componentRegex);
|
labels.add(`component: ${comp}`);
|
||||||
if (componentMatch) {
|
}
|
||||||
labels.add(`component: ${componentMatch[1]}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
for (const file of changedFiles) {
|
||||||
const platformMatch = file.match(targetPlatformRegex);
|
const platformMatch = file.match(targetPlatformRegex);
|
||||||
if (platformMatch) {
|
if (platformMatch) {
|
||||||
labels.add(`platform: ${platformMatch[1]}`);
|
labels.add(`platform: ${platformMatch[1]}`);
|
||||||
@@ -90,15 +95,9 @@ async function detectNewPlatforms(prFiles, apiData) {
|
|||||||
// Strategy: Core files detection
|
// Strategy: Core files detection
|
||||||
async function detectCoreChanges(changedFiles) {
|
async function detectCoreChanges(changedFiles) {
|
||||||
const labels = new Set();
|
const labels = new Set();
|
||||||
const coreFiles = changedFiles.filter(file =>
|
if (hasCoreChanges(changedFiles)) {
|
||||||
file.startsWith('esphome/core/') ||
|
|
||||||
(file.startsWith('esphome/') && file.split('/').length === 2)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (coreFiles.length > 0) {
|
|
||||||
labels.add('core');
|
labels.add('core');
|
||||||
}
|
}
|
||||||
|
|
||||||
return labels;
|
return labels;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,29 +130,18 @@ async function detectPRSize(prFiles, totalAdditions, totalDeletions, totalChange
|
|||||||
// Strategy: Dashboard changes
|
// Strategy: Dashboard changes
|
||||||
async function detectDashboardChanges(changedFiles) {
|
async function detectDashboardChanges(changedFiles) {
|
||||||
const labels = new Set();
|
const labels = new Set();
|
||||||
const dashboardFiles = changedFiles.filter(file =>
|
if (hasDashboardChanges(changedFiles)) {
|
||||||
file.startsWith('esphome/dashboard/') ||
|
|
||||||
file.startsWith('esphome/components/dashboard_import/')
|
|
||||||
);
|
|
||||||
|
|
||||||
if (dashboardFiles.length > 0) {
|
|
||||||
labels.add('dashboard');
|
labels.add('dashboard');
|
||||||
}
|
}
|
||||||
|
|
||||||
return labels;
|
return labels;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strategy: GitHub Actions changes
|
// Strategy: GitHub Actions changes
|
||||||
async function detectGitHubActionsChanges(changedFiles) {
|
async function detectGitHubActionsChanges(changedFiles) {
|
||||||
const labels = new Set();
|
const labels = new Set();
|
||||||
const githubActionsFiles = changedFiles.filter(file =>
|
if (hasGitHubActionsChanges(changedFiles)) {
|
||||||
file.startsWith('.github/workflows/')
|
|
||||||
);
|
|
||||||
|
|
||||||
if (githubActionsFiles.length > 0) {
|
|
||||||
labels.add('github-actions');
|
labels.add('github-actions');
|
||||||
}
|
}
|
||||||
|
|
||||||
return labels;
|
return labels;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,7 +247,7 @@ async function detectDeprecatedComponents(github, context, changedFiles) {
|
|||||||
const { owner, repo } = context.repo;
|
const { owner, repo } = context.repo;
|
||||||
|
|
||||||
// Compile regex once for better performance
|
// Compile regex once for better performance
|
||||||
const componentFileRegex = /^esphome\/components\/([^\/]+)\//;
|
const componentFileRegex = COMPONENT_REGEX;
|
||||||
|
|
||||||
// Get files that are modified or added in components directory
|
// Get files that are modified or added in components directory
|
||||||
const componentFiles = changedFiles.filter(file => componentFileRegex.test(file));
|
const componentFiles = changedFiles.filter(file => componentFileRegex.test(file));
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* Shared tag detection from changed file paths.
|
||||||
|
* Used by pr-title-check and auto-label-pr workflows.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const COMPONENT_REGEX = /^esphome\/components\/([^\/]+)\//;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect component names from changed files.
|
||||||
|
* @param {string[]} changedFiles - List of changed file paths
|
||||||
|
* @returns {Set<string>} Set of component names
|
||||||
|
*/
|
||||||
|
function detectComponents(changedFiles) {
|
||||||
|
const components = new Set();
|
||||||
|
for (const file of changedFiles) {
|
||||||
|
const match = file.match(COMPONENT_REGEX);
|
||||||
|
if (match) {
|
||||||
|
components.add(match[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return components;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect if core files were changed.
|
||||||
|
* Core files are in esphome/core/ or top-level esphome/ directory.
|
||||||
|
* @param {string[]} changedFiles - List of changed file paths
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function hasCoreChanges(changedFiles) {
|
||||||
|
return changedFiles.some(file =>
|
||||||
|
file.startsWith('esphome/core/') ||
|
||||||
|
(file.startsWith('esphome/') && file.split('/').length === 2)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect if dashboard files were changed.
|
||||||
|
* @param {string[]} changedFiles - List of changed file paths
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function hasDashboardChanges(changedFiles) {
|
||||||
|
return changedFiles.some(file =>
|
||||||
|
file.startsWith('esphome/dashboard/') ||
|
||||||
|
file.startsWith('esphome/components/dashboard_import/')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect if GitHub Actions files were changed.
|
||||||
|
* @param {string[]} changedFiles - List of changed file paths
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function hasGitHubActionsChanges(changedFiles) {
|
||||||
|
return changedFiles.some(file =>
|
||||||
|
file.startsWith('.github/workflows/')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
COMPONENT_REGEX,
|
||||||
|
detectComponents,
|
||||||
|
hasCoreChanges,
|
||||||
|
hasDashboardChanges,
|
||||||
|
hasGitHubActionsChanges,
|
||||||
|
};
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
name: PR Title Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, edited, synchronize, reopened]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
name: Validate PR title
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
|
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const {
|
||||||
|
detectComponents,
|
||||||
|
hasCoreChanges,
|
||||||
|
hasDashboardChanges,
|
||||||
|
hasGitHubActionsChanges,
|
||||||
|
} = require('./.github/scripts/detect-tags.js');
|
||||||
|
|
||||||
|
const title = context.payload.pull_request.title;
|
||||||
|
|
||||||
|
// Block titles starting with "word:" or "word(scope):" patterns
|
||||||
|
const commitStylePattern = /^\w+(\(.*?\))?[!]?\s*:/;
|
||||||
|
if (commitStylePattern.test(title)) {
|
||||||
|
core.setFailed(
|
||||||
|
`PR title should not start with a "prefix:" style format.\n` +
|
||||||
|
`Please use the format: [component] Brief description\n` +
|
||||||
|
`Example: [pn532] Add health checking and auto-reset`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get changed files to detect tags
|
||||||
|
const files = await github.paginate(github.rest.pulls.listFiles, {
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
pull_number: context.issue.number,
|
||||||
|
});
|
||||||
|
const filenames = files.map(f => f.filename);
|
||||||
|
|
||||||
|
// Detect tags from changed files using shared logic
|
||||||
|
const tags = new Set();
|
||||||
|
|
||||||
|
for (const comp of detectComponents(filenames)) {
|
||||||
|
tags.add(comp);
|
||||||
|
}
|
||||||
|
if (hasCoreChanges(filenames)) tags.add('core');
|
||||||
|
if (hasDashboardChanges(filenames)) tags.add('dashboard');
|
||||||
|
if (hasGitHubActionsChanges(filenames)) tags.add('ci');
|
||||||
|
|
||||||
|
if (tags.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check title starts with [tag] prefix
|
||||||
|
const bracketPattern = /^\[\w+\]/;
|
||||||
|
if (!bracketPattern.test(title)) {
|
||||||
|
const suggestion = [...tags].map(c => `[${c}]`).join('');
|
||||||
|
// Skip if the suggested prefix would be too long for a readable title
|
||||||
|
if (suggestion.length > 40) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
core.setFailed(
|
||||||
|
`PR modifies: ${[...tags].join(', ')}\n` +
|
||||||
|
`Title must start with a [tag] prefix.\n` +
|
||||||
|
`Suggested: ${suggestion} <description>`
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user