mirror of
https://github.com/esphome/esphome.git
synced 2026-05-30 23:54:04 +08:00
[ci] Skip needs-docs for new components without CONFIG_SCHEMA (#16303)
This commit is contained in:
@@ -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));
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user