265 lines
9.0 KiB
JavaScript
265 lines
9.0 KiB
JavaScript
/**
|
|
* Detect Preview Pages
|
|
* Determines which pages should be included in PR preview based on changed files.
|
|
*
|
|
* Outputs (for GitHub Actions):
|
|
* - pages-to-deploy: JSON array of URL paths to deploy
|
|
* - has-layout-changes: 'true' if layout/asset/data changes detected
|
|
* - has-api-doc-changes: 'true' if api-docs/ or openapi/ changes detected
|
|
* - needs-author-input: 'true' if author must select pages
|
|
* - change-summary: Human-readable summary of changes
|
|
*/
|
|
|
|
import { execSync } from 'child_process';
|
|
import { appendFileSync, existsSync, readFileSync } from 'fs';
|
|
import { load } from 'js-yaml';
|
|
import { extractDocsUrls } from './parse-pr-urls.js';
|
|
import {
|
|
getChangedContentFiles,
|
|
mapContentToPublic,
|
|
} from '../../scripts/lib/content-utils.js';
|
|
|
|
const GITHUB_OUTPUT = process.env.GITHUB_OUTPUT || '/dev/stdout';
|
|
const PR_BODY = process.env.PR_BODY || '';
|
|
const BASE_REF = process.env.BASE_REF || 'origin/master';
|
|
const MAX_PAGES = 50; // Limit to prevent storage bloat
|
|
|
|
// Validate BASE_REF to prevent command injection
|
|
// Allows branch names with letters, numbers, dots, underscores, hyphens, and slashes
|
|
if (!/^origin\/[a-zA-Z0-9._\/-]+$/.test(BASE_REF)) {
|
|
console.error(`Invalid BASE_REF: ${BASE_REF}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
/**
|
|
* Get all changed files in the PR
|
|
* @returns {string[]} Array of changed file paths
|
|
*/
|
|
function getAllChangedFiles() {
|
|
try {
|
|
const output = execSync(`git diff --name-only ${BASE_REF}...HEAD`, {
|
|
encoding: 'utf-8',
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
});
|
|
return output.trim().split('\n').filter(Boolean);
|
|
} catch (err) {
|
|
console.error(`Error detecting changes: ${err.message}`);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Categorize changed files by type
|
|
* @param {string[]} files - All changed files
|
|
* @returns {Object} Categorized files
|
|
*/
|
|
function categorizeChanges(files) {
|
|
return {
|
|
content: files.filter((f) => f.startsWith('content/') && f.endsWith('.md')),
|
|
layouts: files.filter((f) => f.startsWith('layouts/')),
|
|
assets: files.filter((f) => f.startsWith('assets/')),
|
|
data: files.filter((f) => f.startsWith('data/')),
|
|
apiDocs: files.filter(
|
|
(f) => f.startsWith('api-docs/') || f.startsWith('openapi/')
|
|
),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Check if PR only contains deletions (no additions/modifications)
|
|
* @returns {boolean}
|
|
*/
|
|
function isOnlyDeletions() {
|
|
try {
|
|
const output = execSync(
|
|
`git diff --diff-filter=d --name-only ${BASE_REF}...HEAD`,
|
|
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
);
|
|
// If there are no non-deletion changes, this returns empty
|
|
return output.trim() === '';
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Detect API pages affected by changed api-docs files.
|
|
* Maps changed api-docs files to their corresponding content URL paths by reading
|
|
* each product version's .config.yml and extracting API keys.
|
|
* @param {string[]} apiDocFiles - Changed files in api-docs/
|
|
* @returns {string[]} Array of URL paths for affected API pages
|
|
*/
|
|
function detectApiPages(apiDocFiles) {
|
|
const pages = new Set();
|
|
const processedVersions = new Set();
|
|
// Matches the {product}/{version} path segment in api-docs/{product}/{version}/...
|
|
// e.g., api-docs/influxdb3/core/.config.yml -> captures 'influxdb3/core'
|
|
const PRODUCT_VERSION_PATTERN = /^api-docs\/([^/]+\/[^/]+)\//;
|
|
|
|
for (const file of apiDocFiles) {
|
|
const match = file.match(PRODUCT_VERSION_PATTERN);
|
|
if (!match) continue;
|
|
|
|
const productVersionDir = match[1]; // e.g., 'influxdb3/core' or 'influxdb/v2'
|
|
|
|
// Only process each product version once
|
|
if (processedVersions.has(productVersionDir)) continue;
|
|
processedVersions.add(productVersionDir);
|
|
|
|
const configPath = `api-docs/${productVersionDir}/.config.yml`;
|
|
if (!existsSync(configPath)) continue;
|
|
|
|
try {
|
|
const configContent = readFileSync(configPath, 'utf-8');
|
|
const config = load(configContent);
|
|
|
|
if (!config || !config.apis) continue;
|
|
|
|
for (const apiKey of Object.keys(config.apis)) {
|
|
// Extract apiName: e.g., 'v3@3' -> 'v3', 'v1-compatibility@2' -> 'v1-compatibility'
|
|
const apiName = apiKey.split('@')[0];
|
|
const urlPath = `/${productVersionDir}/api/${apiName}/`;
|
|
pages.add(urlPath);
|
|
}
|
|
} catch (err) {
|
|
console.log(` ⚠️ Could not read or parse ${configPath}: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
return Array.from(pages);
|
|
}
|
|
|
|
/**
|
|
* Write output for GitHub Actions
|
|
*/
|
|
function setOutput(name, value) {
|
|
const output = typeof value === 'string' ? value : JSON.stringify(value);
|
|
appendFileSync(GITHUB_OUTPUT, `${name}=${output}\n`);
|
|
console.log(`::set-output name=${name}::${output}`);
|
|
}
|
|
|
|
// Main execution
|
|
function main() {
|
|
console.log('🔍 Detecting changes for PR preview...\n');
|
|
|
|
// Check for deletions-only PR
|
|
if (isOnlyDeletions()) {
|
|
console.log('📭 PR contains only deletions - skipping preview');
|
|
setOutput('pages-to-deploy', '[]');
|
|
setOutput('has-layout-changes', 'false');
|
|
setOutput('has-api-doc-changes', 'false');
|
|
setOutput('needs-author-input', 'false');
|
|
setOutput('change-summary', 'No pages to preview (content removed)');
|
|
setOutput('skip-reason', 'deletions-only');
|
|
return;
|
|
}
|
|
|
|
const allChangedFiles = getAllChangedFiles();
|
|
const changes = categorizeChanges(allChangedFiles);
|
|
|
|
console.log(`📁 Changed files breakdown:`);
|
|
console.log(` Content: ${changes.content.length}`);
|
|
console.log(` Layouts: ${changes.layouts.length}`);
|
|
console.log(` Assets: ${changes.assets.length}`);
|
|
console.log(` Data: ${changes.data.length}`);
|
|
console.log(` API Docs: ${changes.apiDocs.length}\n`);
|
|
|
|
const hasLayoutChanges =
|
|
changes.layouts.length > 0 ||
|
|
changes.assets.length > 0 ||
|
|
changes.data.length > 0 ||
|
|
changes.apiDocs.length > 0;
|
|
|
|
let pagesToDeploy = [];
|
|
|
|
// Strategy 1: Content-only changes - use existing change detection
|
|
if (changes.content.length > 0) {
|
|
console.log('📝 Processing content changes...');
|
|
const expandedContent = getChangedContentFiles(BASE_REF, { verbose: true });
|
|
const htmlPaths = mapContentToPublic(expandedContent, 'public');
|
|
|
|
// Convert HTML paths to URL paths
|
|
pagesToDeploy = Array.from(htmlPaths).map((htmlPath) => {
|
|
return '/' + htmlPath.replace('public/', '').replace('/index.html', '/');
|
|
});
|
|
console.log(` Found ${pagesToDeploy.length} affected pages\n`);
|
|
}
|
|
|
|
// Strategy 2: API doc changes - auto-detect affected API pages
|
|
if (changes.apiDocs.length > 0) {
|
|
console.log('📋 API doc changes detected, auto-detecting affected pages...');
|
|
const apiPages = detectApiPages(changes.apiDocs);
|
|
if (apiPages.length > 0) {
|
|
console.log(` Found ${apiPages.length} affected API page(s): ${apiPages.join(', ')}`);
|
|
pagesToDeploy = [...new Set([...pagesToDeploy, ...apiPages])];
|
|
}
|
|
}
|
|
|
|
// Strategy 3: Layout/asset changes - parse URLs from PR body
|
|
if (hasLayoutChanges) {
|
|
console.log(
|
|
'🎨 Layout/asset changes detected, checking PR description for URLs...'
|
|
);
|
|
|
|
// Auto-detect home page when the root template changes
|
|
if (changes.layouts.includes('layouts/index.html')) {
|
|
pagesToDeploy = [...new Set([...pagesToDeploy, '/'])];
|
|
console.log(
|
|
' 🏠 Home page template (layouts/index.html) changed - auto-adding / to preview pages'
|
|
);
|
|
}
|
|
|
|
const prUrls = extractDocsUrls(PR_BODY);
|
|
|
|
if (prUrls.length > 0) {
|
|
console.log(` Found ${prUrls.length} URLs in PR description`);
|
|
// Merge with content pages (deduplicate)
|
|
pagesToDeploy = [...new Set([...pagesToDeploy, ...prUrls])];
|
|
<<<<<<< api-docs-uplift
|
|
} else if (changes.content.length === 0 && pagesToDeploy.length === 0) {
|
|
// No content changes, no auto-detected pages, and no URLs specified - need author input
|
|
console.log(' ⚠️ No URLs found in PR description - author input needed');
|
|
=======
|
|
} else if (pagesToDeploy.length === 0) {
|
|
// No content changes, no auto-detected pages, and no URLs specified - need author input
|
|
console.log(
|
|
' ⚠️ No URLs found in PR description - author input needed'
|
|
);
|
|
>>>>>>> master
|
|
setOutput('pages-to-deploy', '[]');
|
|
setOutput('has-layout-changes', 'true');
|
|
setOutput('has-api-doc-changes', String(changes.apiDocs.length > 0));
|
|
setOutput('needs-author-input', 'true');
|
|
setOutput(
|
|
'change-summary',
|
|
'Layout/asset changes detected - please specify pages to preview'
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Apply page limit
|
|
if (pagesToDeploy.length > MAX_PAGES) {
|
|
console.log(
|
|
`⚠️ Limiting preview to ${MAX_PAGES} pages (found ${pagesToDeploy.length})`
|
|
);
|
|
pagesToDeploy = pagesToDeploy.slice(0, MAX_PAGES);
|
|
}
|
|
|
|
// Generate summary
|
|
const summary =
|
|
pagesToDeploy.length > 0
|
|
? `${pagesToDeploy.length} page(s) will be previewed`
|
|
: 'No pages to preview';
|
|
|
|
console.log(`\n✅ ${summary}`);
|
|
|
|
setOutput('pages-to-deploy', JSON.stringify(pagesToDeploy));
|
|
setOutput('has-layout-changes', String(hasLayoutChanges));
|
|
setOutput('has-api-doc-changes', String(changes.apiDocs.length > 0));
|
|
setOutput('needs-author-input', 'false');
|
|
setOutput('change-summary', summary);
|
|
}
|
|
|
|
main();
|