diff --git a/.github/workflows/markdown-lint-fix.yml b/.github/workflows/auto-cleanup-bot.yml similarity index 85% rename from .github/workflows/markdown-lint-fix.yml rename to .github/workflows/auto-cleanup-bot.yml index feaeb7893138064..058db6f36e88d38 100644 --- a/.github/workflows/markdown-lint-fix.yml +++ b/.github/workflows/auto-cleanup-bot.yml @@ -1,4 +1,4 @@ -name: Create Markdownlint auto-fix PR +name: Create content auto-fix PR on: schedule: @@ -31,14 +31,15 @@ jobs: yarn content fix-flaws yarn fix:md yarn fix:fm + node scripts/update-moved-file-links.js - name: Create PR with only fixable issues if: success() uses: peter-evans/create-pull-request@v5 with: - commit-message: "chore: auto-fix Markdownlint issues" + commit-message: "chore: auto-fix Markdownlint, Prettier, front-matter, redirects issues" branch: markdownlint-auto-cleanup - title: "Markdownlint auto-cleanup" + title: "fix: auto-cleanup by bot" author: mdn-bot <108879845+mdn-bot@users.noreply.github.com> body: | All issues auto-fixed @@ -50,7 +51,7 @@ jobs: with: commit-message: "chore: auto-fix Markdownlint issues" branch: markdownlint-auto-cleanup - title: "Markdownlint auto-cleanup" + title: "fix: auto-cleanup by bot" author: mdn-bot <108879845+mdn-bot@users.noreply.github.com> body: | Auto-fix was run, but additional issues found. diff --git a/scripts/update-moved-file-links.js b/scripts/update-moved-file-links.js new file mode 100644 index 000000000000000..264a9d4aa562ce0 --- /dev/null +++ b/scripts/update-moved-file-links.js @@ -0,0 +1,138 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { execGit, getRootDir, walkSync, isImagePath } from "./utils.js"; + +const SLUG_RX = /(?<=\nslug: ).*?$/gm; +const HELP_MSG = + "Usage:\n\t" + + "node scripts/update-moved-file-links.js\n\t" + + "node scripts/update-moved-file-links.js [movedFromPath] [movedToPath]\n"; + +/** + * Try to get slug for an image from file path + */ +export async function getImageSlug(imagePath, root) { + const nodePath = path.parse(imagePath); + const absolutePath = `${root}/files/en-us/${nodePath.dir}/index.md`; + let content; + try { + content = await fs.readFile(absolutePath, "utf-8"); + } catch (e) {} + + if (content) { + return `/en-US/docs/${(content.match(SLUG_RX) || [])[0]}/${nodePath.base}`; + } else { + return `/en-US/docs/${imagePath}`; + } +} + +let movedFiles = []; +const rootDir = getRootDir(); +const argLength = process.argv.length; + +if (process.argv[2] === "--help" || process.argv[2] === "-h") { + console.error(HELP_MSG); + process.exit(0); +} else if (argLength === 2 && argLength > 3) { + console.error(HELP_MSG); + process.exit(1); +} else if (argLength === 3) { + movedFiles.push({ from: process.argv[2], to: process.argv[3] }); +} else { + // git log --name-status --pretty=format:"" --since "1 day ago" --diff-filter=R + let result = execGit( + [ + "log", + "--name-status", + "--pretty=format:", + '--since="1 day ago"', + "--diff-filter=R", + ], + { cwd: "." }, + ); + + if (result.trim()) { + movedFiles.push( + ...result + .split("\n") + .filter((line) => line.trim() !== "" && line.includes("files/en-us")) + .map((line) => line.replaceAll(/files\/en-us\/|\/index.md/gm, "")) + .map((line) => line.split(/\s/)) + .map((tuple) => { + return { from: tuple[1], to: tuple[2] }; + }), + ); + } +} + +if (movedFiles.length < 1) { + console.log("No content files were moved. Nothing to update! 🎉"); + process.exit(0); +} + +const redirectsText = await fs.readFile( + `${rootDir}/files/en-us/_redirects.txt`, + "utf-8", +); + +// convert file paths to slugs +movedFiles = ( + await Promise.all( + movedFiles.map(async (tuple) => { + const movedLineRg = new RegExp(`\n.*?${tuple.from}\\s+.*?\n`, "gmi"); + const redirectLine = (redirectsText.match(movedLineRg) || [])[0]; + + if (redirectLine) { + const urls = redirectLine.trim().split(/\s+/); + return { from: urls[0], to: urls[1] }; + } + + if (isImagePath(tuple.from)) { + return { + from: await getImageSlug(tuple.from, rootDir), + to: await getImageSlug(tuple.to, rootDir), + }; + } + + console.warn("No redirect entry found for: ", tuple.from); + }), + ) +).filter((e) => !!e); + +console.log(`Number of moved files to consider: ${movedFiles.length}`); + +let totalNo = 0; +let updatedNo = 0; +for await (const filePath of walkSync(getRootDir())) { + if (filePath.endsWith("index.md")) { + try { + totalNo++; + const content = await fs.readFile(filePath, "utf-8"); + let updated = new String(content); + for (const moved of movedFiles) { + // [text](link) + updated = updated.replaceAll(`${moved.from})`, `${moved.to})`); + // + updated = updated.replaceAll(`${moved.from}>`, `${moved.to}>`); + // [text](link#) + updated = updated.replaceAll(`${moved.from}#`, `${moved.to}#`); + // [text](link "tool tip") + updated = updated.replaceAll(`${moved.from} `, `${moved.to} `); + // + updated = updated.replaceAll(`${moved.from}"`, `${moved.to}"`); + // + updated = updated.replaceAll(`${moved.from}'`, `${moved.to}'`); + } + + if (content !== updated) { + updatedNo++; + await fs.writeFile(filePath, updated); + } + } catch (e) { + console.error(`Error processing ${filePath}: ${e.message}`); + throw e; + } + } +} + +console.log(`Updated moved file links in ${updatedNo}/${totalNo} files.`); diff --git a/scripts/utils.js b/scripts/utils.js new file mode 100644 index 000000000000000..3a8bb5e692f4751 --- /dev/null +++ b/scripts/utils.js @@ -0,0 +1,51 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import childProcess from "node:child_process"; + +const IMG_RX = /(\.png|\.jpg|\.svg|\.gif)$/gim; + +export async function* walkSync(dir) { + const files = await fs.readdir(dir, { withFileTypes: true }); + for (const file of files) { + if (file.isDirectory()) { + yield* walkSync(path.join(dir, file.name)); + } else { + yield path.join(dir, file.name); + } + } +} + +export function execGit(args, opts = {}, root = null) { + const gitRoot = root || getRootDir(); + const { status, error, stdout, stderr } = childProcess.spawnSync( + "git", + args, + { + cwd: gitRoot, + // Default is 1MB + maxBuffer: 1024 * 1024 * 100, // 100MB + }, + ); + if (error || status !== 0) { + if (stderr) { + console.log(args); + console.log(`Error running git ${args}`); + console.error(stderr); + } + if (error) { + throw error; + } + throw new Error( + `git command failed: ${stderr.toString() || stdout.toString()}`, + ); + } + return stdout.toString().trim(); +} + +export function getRootDir() { + return execGit(["rev-parse", "--show-toplevel"], {}, process.cwd()); +} + +export function isImagePath(path) { + return IMG_RX.test(path); +}