forked from mdn/content
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(bot): auto update links of moved files (mdn#31091)
* feat(bot): auto update links of moved files * Update scripts/update-moved-file-links.js Co-authored-by: Jean-Yves Perrier <[email protected]> --------- Co-authored-by: Jean-Yves Perrier <[email protected]>
- Loading branch information
Showing
3 changed files
with
194 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <[email protected]> | ||
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 <[email protected]> | ||
body: | | ||
Auto-fix was run, but additional issues found. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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})`); | ||
// <link> | ||
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} `); | ||
// <a href="link"> | ||
updated = updated.replaceAll(`${moved.from}"`, `${moved.to}"`); | ||
// <a href='link'> | ||
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.`); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} |