chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,285 @@
|
||||
/**
|
||||
* 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 });
|
||||
};
|
||||
Reference in New Issue
Block a user