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);
+}