[ci] Skip needs-docs for new components without CONFIG_SCHEMA (#16303)

This commit is contained in:
Jesse Hills
2026-05-08 14:00:50 +12:00
committed by GitHub
parent 6ffcb821ca
commit e152c6155b
2 changed files with 86 additions and 36 deletions
+74 -31
View File
@@ -1,4 +1,3 @@
const fs = require('fs');
const { DOCS_PR_PATTERNS } = require('./constants'); const { DOCS_PR_PATTERNS } = require('./constants');
const { const {
COMPONENT_REGEX, COMPONENT_REGEX,
@@ -9,6 +8,31 @@ const {
} = require('../detect-tags'); } = require('../detect-tags');
const { loadCodeowners, getEffectiveOwners } = require('../codeowners'); const { loadCodeowners, getEffectiveOwners } = require('../codeowners');
// Top-level `CONFIG_SCHEMA = ...` (assignment) or `CONFIG_SCHEMA: ConfigType = ...` (annotation).
// Ruff/Black enforce exactly one space around `=` and no space before `:`,
// so we can match strictly: `CONFIG_SCHEMA ` or `CONFIG_SCHEMA:`.
const CONFIG_SCHEMA_REGEX = /^CONFIG_SCHEMA[ :]/m;
// Fetch a file's contents from the PR head SHA via the GitHub API.
// The auto-label workflow runs on `pull_request_target`, which checks out the
// base branch — files added by the PR don't exist in the workspace, so we have
// to fetch them from the head SHA. Returns null if the file can't be fetched.
async function fetchPrFileContent(github, context, path) {
try {
const { owner, repo } = context.repo;
const { data } = await github.rest.repos.getContent({
owner,
repo,
path,
ref: context.payload.pull_request.head.sha,
});
return Buffer.from(data.content, 'base64').toString('utf8');
} catch (error) {
console.log(`Failed to fetch ${path} from PR head:`, error.message);
return null;
}
}
// Strategy: Merge branch detection // Strategy: Merge branch detection
async function detectMergeBranch(context) { async function detectMergeBranch(context) {
const labels = new Set(); const labels = new Set();
@@ -45,52 +69,64 @@ async function detectComponentPlatforms(changedFiles, apiData) {
} }
// Strategy: New component detection // Strategy: New component detection
async function detectNewComponents(prFiles) { async function detectNewComponents(github, context, prFiles) {
const labels = new Set(); const labels = new Set();
let hasYamlLoadable = false;
const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename); const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename);
for (const file of addedFiles) { for (const file of addedFiles) {
const componentMatch = file.match(/^esphome\/components\/([^\/]+)\/__init__\.py$/); const componentMatch = file.match(/^esphome\/components\/([^\/]+)\/__init__\.py$/);
if (componentMatch) { if (!componentMatch) continue;
try {
const content = fs.readFileSync(file, 'utf8'); labels.add('new-component');
if (content.includes('IS_TARGET_PLATFORM = True')) { const content = await fetchPrFileContent(github, context, file);
labels.add('new-target-platform'); if (content === null) {
} // Safe default: assume YAML-loadable so needs-docs behaviour is unchanged on fetch failure
} catch (error) { hasYamlLoadable = true;
console.log(`Failed to read content of ${file}:`, error.message); continue;
} }
labels.add('new-component'); if (content.includes('IS_TARGET_PLATFORM = True')) {
labels.add('new-target-platform');
}
if (CONFIG_SCHEMA_REGEX.test(content)) {
hasYamlLoadable = true;
} }
} }
return labels; return { labels, hasYamlLoadable };
} }
// Strategy: New platform detection // Strategy: New platform detection
async function detectNewPlatforms(prFiles, apiData) { async function detectNewPlatforms(github, context, prFiles, apiData) {
const labels = new Set(); const labels = new Set();
let hasYamlLoadable = false;
const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename); const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename);
for (const file of addedFiles) { const platformPathPatterns = [
const platformFileMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\.py$/); /^esphome\/components\/([^\/]+)\/([^\/]+)\.py$/,
if (platformFileMatch) { /^esphome\/components\/([^\/]+)\/([^\/]+)\/__init__\.py$/,
const [, component, platform] = platformFileMatch; ];
if (apiData.platformComponents.includes(platform)) {
labels.add('new-platform');
}
}
const platformDirMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\/__init__\.py$/); for (const file of addedFiles) {
if (platformDirMatch) { for (const re of platformPathPatterns) {
const [, component, platform] = platformDirMatch; const match = file.match(re);
if (apiData.platformComponents.includes(platform)) { if (!match) continue;
labels.add('new-platform'); const platform = match[2];
if (!apiData.platformComponents.includes(platform)) break;
labels.add('new-platform');
const content = await fetchPrFileContent(github, context, file);
if (content === null) {
// Safe default: assume YAML-loadable so needs-docs behaviour is unchanged on fetch failure
hasYamlLoadable = true;
} else if (CONFIG_SCHEMA_REGEX.test(content)) {
hasYamlLoadable = true;
} }
break;
} }
} }
return labels; return { labels, hasYamlLoadable };
} }
// Strategy: Core files detection // Strategy: Core files detection
@@ -300,7 +336,7 @@ function detectMaintainerAccess(context) {
} }
// Strategy: Requirements detection // Strategy: Requirements detection
async function detectRequirements(allLabels, prFiles, context) { async function detectRequirements(allLabels, prFiles, context, hasYamlLoadable) {
const labels = new Set(); const labels = new Set();
// Check for missing tests // Check for missing tests
@@ -308,8 +344,15 @@ async function detectRequirements(allLabels, prFiles, context) {
labels.add('needs-tests'); labels.add('needs-tests');
} }
// Check for missing docs // Check for missing docs.
if (allLabels.has('new-component') || allLabels.has('new-platform') || allLabels.has('new-feature')) { // `new-feature` (PR-body checkbox) always counts. `new-component` / `new-platform`
// only count when at least one newly added file defines a top-level CONFIG_SCHEMA,
// i.e. the new component/platform is actually loadable from YAML.
const docsEligible =
allLabels.has('new-feature') ||
((allLabels.has('new-component') || allLabels.has('new-platform')) && hasYamlLoadable);
if (docsEligible) {
const prBody = context.payload.pull_request.body || ''; const prBody = context.payload.pull_request.body || '';
const hasDocsLink = DOCS_PR_PATTERNS.some(pattern => pattern.test(prBody)); const hasDocsLink = DOCS_PR_PATTERNS.some(pattern => pattern.test(prBody));
+12 -5
View File
@@ -106,8 +106,8 @@ module.exports = async ({ github, context }) => {
const [ const [
branchLabels, branchLabels,
componentLabels, componentLabels,
newComponentLabels, newComponentResult,
newPlatformLabels, newPlatformResult,
coreLabels, coreLabels,
sizeLabels, sizeLabels,
dashboardLabels, dashboardLabels,
@@ -120,8 +120,8 @@ module.exports = async ({ github, context }) => {
] = await Promise.all([ ] = await Promise.all([
detectMergeBranch(context), detectMergeBranch(context),
detectComponentPlatforms(changedFiles, apiData), detectComponentPlatforms(changedFiles, apiData),
detectNewComponents(prFiles), detectNewComponents(github, context, prFiles),
detectNewPlatforms(prFiles, apiData), detectNewPlatforms(github, context, prFiles, apiData),
detectCoreChanges(changedFiles), detectCoreChanges(changedFiles),
detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, MEDIUM_PR_THRESHOLD, TOO_BIG_THRESHOLD), detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, MEDIUM_PR_THRESHOLD, TOO_BIG_THRESHOLD),
detectDashboardChanges(changedFiles), detectDashboardChanges(changedFiles),
@@ -133,6 +133,13 @@ module.exports = async ({ github, context }) => {
detectMaintainerAccess(context) detectMaintainerAccess(context)
]); ]);
// Extract new-component / new-platform results
const newComponentLabels = newComponentResult.labels;
const newPlatformLabels = newPlatformResult.labels;
// Eligible for needs-docs only if any newly added component or platform file
// defines a top-level CONFIG_SCHEMA (i.e. is actually loadable from YAML).
const hasYamlLoadable = newComponentResult.hasYamlLoadable || newPlatformResult.hasYamlLoadable;
// Extract deprecated component info // Extract deprecated component info
const deprecatedLabels = deprecatedResult.labels; const deprecatedLabels = deprecatedResult.labels;
const deprecatedInfo = deprecatedResult.deprecatedInfo; const deprecatedInfo = deprecatedResult.deprecatedInfo;
@@ -154,7 +161,7 @@ module.exports = async ({ github, context }) => {
]); ]);
// Detect requirements based on all other labels // Detect requirements based on all other labels
const requirementLabels = await detectRequirements(allLabels, prFiles, context); const requirementLabels = await detectRequirements(allLabels, prFiles, context, hasYamlLoadable);
for (const label of requirementLabels) { for (const label of requirementLabels) {
allLabels.add(label); allLabels.add(label);
} }