286 lines
8.8 KiB
JavaScript
286 lines
8.8 KiB
JavaScript
/**
|
|
* Script to manage documentation preview comments on pull requests.
|
|
*/
|
|
|
|
const path = require("path");
|
|
|
|
const MARKER = "<!-- documentation preview -->";
|
|
|
|
/**
|
|
* Check if a URL is accessible
|
|
* @param {string} url - URL to check
|
|
* @returns {Promise<boolean|undefined>} True if the URL is accessible (200 or 3xx), false otherwise; undefined if an error occurs
|
|
*/
|
|
async function isUrlAccessible(url) {
|
|
try {
|
|
const res = await fetch(url, { method: "GET", redirect: "manual" });
|
|
// treat 200 and 3xx as "accessible", 404 as "missing"
|
|
return res.ok || (res.status >= 300 && res.status < 400);
|
|
} catch (e) {
|
|
console.error(`Error checking accessibility for ${url}:`, e);
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch changed files from a pull request
|
|
* @param {object} params - Parameters object
|
|
* @param {object} params.github - GitHub API client
|
|
* @param {string} params.owner - Repository owner
|
|
* @param {string} params.repo - Repository name
|
|
* @param {string} params.pullNumber - Pull request number
|
|
* @returns {Promise<Array<{filename: string, status: string}>>} Array of changed file objects with filename and status
|
|
*/
|
|
async function fetchChangedFiles({ github, owner, repo, pullNumber }) {
|
|
const iterator = github.paginate.iterator(github.rest.pulls.listFiles, {
|
|
owner,
|
|
repo,
|
|
pull_number: pullNumber,
|
|
per_page: 100,
|
|
});
|
|
|
|
const changedFiles = [];
|
|
for await (const { data } of iterator) {
|
|
changedFiles.push(...data.map(({ filename, status }) => ({ filename, status })));
|
|
}
|
|
|
|
return changedFiles;
|
|
}
|
|
|
|
/**
|
|
* Get changed documentation pages from the list of changed files
|
|
* @param {Array<{filename: string, status: string}>} changedFiles - Array of changed file objects
|
|
* @returns {Array<{page: string, status: string}>} Array of documentation page objects with page path and status, sorted alphabetically by page path
|
|
*/
|
|
function getChangedDocPages(changedFiles) {
|
|
const DOCS_DIR = "docs/docs/";
|
|
const changedPages = [];
|
|
|
|
for (const { filename, status } of changedFiles) {
|
|
const ext = path.extname(filename);
|
|
if (ext !== ".md" && ext !== ".mdx") continue;
|
|
if (!filename.startsWith(DOCS_DIR)) continue;
|
|
|
|
const relativePath = path.relative(DOCS_DIR, filename);
|
|
const { dir, name, base } = path.parse(relativePath);
|
|
|
|
let pagePath;
|
|
if (base === "index.mdx") {
|
|
pagePath = dir;
|
|
} else {
|
|
pagePath = path.join(dir, name);
|
|
}
|
|
|
|
// Adjust classic-ml/ to ml/
|
|
pagePath = pagePath.replace(/^classic-ml/, "ml");
|
|
|
|
// Ensure forward slashes for web paths
|
|
pagePath = pagePath.split(path.sep).join("/");
|
|
|
|
changedPages.push({ page: pagePath, status });
|
|
}
|
|
|
|
// Sort alphabetically by page path for easier lookup
|
|
changedPages.sort((a, b) => a.page.localeCompare(b.page));
|
|
|
|
return changedPages;
|
|
}
|
|
|
|
/**
|
|
* Create or update a PR comment with documentation preview information
|
|
* @param {object} params - Parameters object
|
|
* @param {object} params.github - GitHub API client
|
|
* @param {string} params.owner - Repository owner
|
|
* @param {string} params.repo - Repository name
|
|
* @param {string} params.pullNumber - Pull request number
|
|
* @param {string} params.commentBody - Comment body content
|
|
*/
|
|
async function upsertComment({ github, owner, repo, pullNumber, commentBody }) {
|
|
// Get existing comments on the PR
|
|
const { data: comments } = await github.rest.issues.listComments({
|
|
owner,
|
|
repo,
|
|
issue_number: pullNumber,
|
|
per_page: 100,
|
|
});
|
|
|
|
// Find existing preview docs comment
|
|
const existingComment = comments.find((comment) => comment.body.includes(MARKER));
|
|
const commentBodyWithMarker = `${MARKER}\n\n${commentBody}`;
|
|
|
|
if (!existingComment) {
|
|
console.log("Creating comment");
|
|
await github.rest.issues.createComment({
|
|
owner,
|
|
repo,
|
|
issue_number: pullNumber,
|
|
body: commentBodyWithMarker,
|
|
});
|
|
} else {
|
|
console.log("Updating comment");
|
|
await github.rest.issues.updateComment({
|
|
owner,
|
|
repo,
|
|
comment_id: existingComment.id,
|
|
body: commentBodyWithMarker,
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate the comment template for documentation preview
|
|
* @param {object} params - Parameters object
|
|
* @param {string} params.commitSha - Git commit SHA
|
|
* @param {string} params.workflowRunLink - Link to the workflow run
|
|
* @param {string} params.docsWorkflowRunUrl - Link to the docs workflow run
|
|
* @param {string} params.mainMessage - Main message content
|
|
* @param {Array<{link: string, status: string, isAccessible?: boolean}>} params.changedPages - Array of changed documentation page objects with link, status, and optional accessibility flag
|
|
* @returns {string} Comment template
|
|
*/
|
|
function getCommentTemplate({
|
|
commitSha,
|
|
workflowRunLink,
|
|
docsWorkflowRunUrl,
|
|
mainMessage,
|
|
changedPages,
|
|
}) {
|
|
let changedPagesSection = "";
|
|
|
|
if (changedPages && changedPages.length > 0) {
|
|
const pageLinks = changedPages
|
|
.map(({ link, status, isAccessible }) => {
|
|
let statusText = status;
|
|
// Add warning or success indicator for removed and renamed files
|
|
if (status === "removed" || status === "renamed") {
|
|
if (isAccessible === true) {
|
|
statusText = `${status}, ✅ redirect exists`;
|
|
} else if (isAccessible === false) {
|
|
statusText = `${status}, ❌ add a redirect`;
|
|
} else {
|
|
// If accessibility check failed
|
|
statusText = `${status}, ⚠️ failed to check`;
|
|
}
|
|
}
|
|
return `- ${link} (${statusText})`;
|
|
})
|
|
.join("\n");
|
|
|
|
// Only collapse if there are more than 5 changed pages
|
|
if (changedPages.length > 5) {
|
|
changedPagesSection = `
|
|
|
|
<details>
|
|
<summary>Changed Pages (${changedPages.length})</summary>
|
|
|
|
${pageLinks}
|
|
|
|
</details>
|
|
`;
|
|
} else {
|
|
changedPagesSection = `
|
|
|
|
**Changed Pages (${changedPages.length})**
|
|
|
|
${pageLinks}
|
|
`;
|
|
}
|
|
}
|
|
|
|
return `
|
|
Documentation preview for ${commitSha} ${mainMessage}
|
|
${changedPagesSection}
|
|
<details>
|
|
<summary>More info</summary>
|
|
|
|
- Ignore this comment if this PR does not change the documentation.
|
|
- The preview is updated when a new commit is pushed to this PR.
|
|
- This comment was created by [this workflow run](${workflowRunLink}).
|
|
- The documentation was built by [this workflow run](${docsWorkflowRunUrl}).
|
|
|
|
</details>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* Main function to handle documentation preview comments
|
|
* @param {object} params - Parameters object containing context and github
|
|
* @param {object} params.github - GitHub API client
|
|
* @param {object} params.context - GitHub context
|
|
* @param {object} params.env - Environment variables
|
|
*/
|
|
module.exports = async ({ github, context, env }) => {
|
|
const commitSha = env.COMMIT_SHA;
|
|
const pullNumber = env.PULL_NUMBER;
|
|
const workflowRunId = env.WORKFLOW_RUN_ID;
|
|
const stage = env.STAGE;
|
|
const netlifyUrl = env.NETLIFY_URL;
|
|
const docsWorkflowRunUrl = env.DOCS_WORKFLOW_RUN_URL;
|
|
|
|
// Validate required parameters
|
|
if (!commitSha || !pullNumber || !workflowRunId || !stage || !docsWorkflowRunUrl) {
|
|
throw new Error(
|
|
"Missing required parameters: commit-sha, pull-number, workflow-run-id, stage, docs-workflow-run-url"
|
|
);
|
|
}
|
|
|
|
if (!["completed", "failed"].includes(stage)) {
|
|
throw new Error("Stage must be either 'completed' or 'failed'");
|
|
}
|
|
|
|
if (stage === "completed" && !netlifyUrl) {
|
|
throw new Error("netlify-url is required for completed stage");
|
|
}
|
|
|
|
const { owner, repo } = context.repo;
|
|
const workflowRunLink = `https://github.com/${owner}/${repo}/actions/runs/${workflowRunId}`;
|
|
|
|
let mainMessage;
|
|
let changedPages = [];
|
|
|
|
if (stage === "completed") {
|
|
mainMessage = `is available at:\n\n- ${netlifyUrl}`;
|
|
|
|
// Fetch changed files and get documentation pages
|
|
try {
|
|
const changedFiles = await fetchChangedFiles({ github, owner, repo, pullNumber });
|
|
const docPages = getChangedDocPages(changedFiles);
|
|
|
|
// Convert to clickable links with status if we have changed pages
|
|
if (docPages.length > 0) {
|
|
changedPages = await Promise.all(
|
|
docPages.map(async ({ page, status }) => {
|
|
const pageUrl = `${netlifyUrl}/${page}`;
|
|
const link = `[${page}](${pageUrl})`;
|
|
|
|
// Check accessibility for removed or renamed pages
|
|
let isAccessible;
|
|
if (status === "removed" || status === "renamed") {
|
|
isAccessible = await isUrlAccessible(pageUrl);
|
|
}
|
|
|
|
return {
|
|
link,
|
|
status,
|
|
isAccessible,
|
|
};
|
|
})
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.error("Error fetching changed files:", error);
|
|
// Continue without changed pages list
|
|
}
|
|
} else if (stage === "failed") {
|
|
mainMessage = "failed to build or deploy.";
|
|
}
|
|
|
|
const commentBody = getCommentTemplate({
|
|
commitSha,
|
|
workflowRunLink,
|
|
docsWorkflowRunUrl,
|
|
mainMessage,
|
|
changedPages,
|
|
});
|
|
await upsertComment({ github, owner, repo, pullNumber, commentBody });
|
|
};
|