Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: feat(husky): check deletions and broken fragments in URLs #28

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/pr-check_url-issues.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Check URL issues

on:
pull_request:
branches:
- main
paths:
- "files/**/*.md"

jobs:
check_url_issues:
#if: github.repository == 'mdn/content'
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
cache: yarn

- name: Check URL deletions and broken fragments
run: |
echo "::add-matcher::.github/workflows/url-issues-problem-matcher.json"
git fetch origin main
node scripts/log-url-issues.js --workflow
18 changes: 18 additions & 0 deletions .github/workflows/url-issues-problem-matcher.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"problemMatcher": [
{
"owner": "log-url-issues",
"severity": "error",
"pattern": [
{
"regexp": "^(ERROR|WARN|INFO):(.+):(\\d+):(\\d+):(.+)$",
"severity": 1,
"file": 2,
"line": 3,
"column": 4,
"message": 5
}
]
}
]
}
3 changes: 2 additions & 1 deletion .lintstagedrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
"prettier --write"
],
"tests/**/*.*": "yarn test:front-matter-linter",
"*.{svg,png,jpeg,jpg,gif}": "yarn filecheck"
"*.{svg,png,jpeg,jpg,gif}": "yarn filecheck",
"*": "node scripts/log-url-issues.js"
}
10 changes: 0 additions & 10 deletions files/en-us/_wikihistory.json
Original file line number Diff line number Diff line change
Expand Up @@ -3600,16 +3600,6 @@
"panaggio"
]
},
"Glossary/Nullish": {
"modified": "2020-12-14T00:17:04.905Z",
"contributors": [
"fscholz",
"subbaraju",
"priyagupta314",
"dd-pardal",
"ExE-Boss"
]
},
"Glossary/Number": {
"modified": "2020-04-11T14:21:24.574Z",
"contributors": [
Expand Down
9 changes: 0 additions & 9 deletions files/en-us/glossary/nullish/index.md

This file was deleted.

2 changes: 1 addition & 1 deletion files/en-us/mdn/community/issues/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@

These are the general steps for working on an issue:

1. **Find an issue:** If you're looking to contribute, search for issues with [`good first issue`, `help wanted`](#set_other_labels) or [`p3`](#set_a_priority_label) label. Most repositories have issues with these labels. You are welcome to browse and pick an issue that is suitable for your skill set. Another useful place to look for issues to work on is the [MDN Contributors Task Board](https://github.com/orgs/mdn/projects/25). This project view lists open issues from multiple repositories. You can filter the list based on the topics (`Labels` column) you're interested in. See the description of some of the [labels](#set_other_labels) that get applied during the issue triage process.

Check failure on line 101 in files/en-us/mdn/community/issues/index.md

View workflow job for this annotation

GitHub Actions / check_url_issues

URL fragment 'mdn/community/issues#set_other_labels' is broken

Check failure on line 101 in files/en-us/mdn/community/issues/index.md

View workflow job for this annotation

GitHub Actions / check_url_issues

URL fragment 'mdn/community/issues#set_other_labels' is broken

> **Note:** An issue with the `needs triage` label indicates that the MDN Web Docs core team has not reviewed the issue yet, and you shouldn't begin work on it.

Expand Down Expand Up @@ -212,7 +212,7 @@
- Update the compatibility data at Link-X
```

#### Set other labels
#### Set other labels1

Next, set the following labels as appropriate:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ new Date(year, monthIndex, day, hours, minutes, seconds, milliseconds)

##### Formal syntax

Formal syntax notation (using [BNF](https://en.wikipedia.org/wiki/Backus%E2%80%93Naur_form)) should not be used in the Syntax section — instead use the expanded multiple-line format [described above](multiple_linesoptional_parameters).
Formal syntax notation (using [BNF](https://en.wikipedia.org/wiki/Backus%E2%80%93Naur_form)) should not be used in the Syntax section — instead use the expanded multiple-line format [described above](#multiple_linesoptional_parameters).

While the formal notation provides a concise mechanism for describing complex syntax, it is not familiar to many developers, and can _conflict_ with valid syntax for particular programming languages. For example, "`[ ]`" indicates both an "optional parameter" and a JavaScript {{jsxref("Array")}}. You can see this in the formal syntax for {{jsxref("Array.prototype.slice()")}} below:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ browser-compat: webextensions.api.runtime.setUninstallURL

{{AddonSidebar()}}

Sets the URL to be visited when the extension is uninstalled. This can be used to clean up server-side data, do analytics, or implement surveys. The URL can be up to 1023 characters. This limit used to be 255, see [Browser compatibility](browser_compatibility) for more details.
Sets the URL to be visited when the extension is uninstalled. This can be used to clean up server-side data, do analytics, or implement surveys. The URL can be up to 1023 characters. This limit used to be 255, see [Browser compatibility](#browser_compatibility) for more details.

This is an asynchronous function that returns a [`Promise`](/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise).

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const s = Boolean(myString); // initial value of true

> **Warning:** You should rarely find yourself using `Boolean` as a constructor.

### Boolean coercion
### Boolean coercion1

Many built-in operations that expect booleans first coerce their arguments to booleans. [The operation](https://tc39.es/ecma262/multipage/abstract-operations.html#sec-toboolean) can be summarized as follows:

Expand Down
140 changes: 140 additions & 0 deletions scripts/log-url-issues.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*
* The script logs locations of affected URLs due to following reasons:
* - file deletion
* - Markdown header updates
*/

import fs from "node:fs/promises";
import path from "node:path";
import {
execGit,
getRootDir,
walkSync,
isImagePath,
getLocations,
IMG_RX,
stringToFragment,
} from "./utils.js";

const rootDir = getRootDir();
const argLength = process.argv.length;
const deletedSlugs = [];
const fragmentDetails = [];
let isAllOk = true;

function getDeletedSlugs() {
// git status --short --porcelain
let result = execGit(["status", "--short", "--porcelain"], { cwd: "." });

if (result.trim()) {
deletedSlugs.push(
...result
.split("\n")
.filter(
(line) =>
/^\s*D\s+/gi.test(line) &&
line.includes("files/en-us") &&
(IMG_RX.test(line) || line.includes("index.md")),
)
.map((line) => line.replaceAll(/^\s*|files\/en-us\/|\/index.md/gm, ""))
.map((line) => line.split(/\s+/)[1]),
);
}
console.log("deletedSlugs", deletedSlugs);
}

function getFragmentDetails(fromStaging = true) {
let result = "";

if (fromStaging) {
// get staged and unstaged changes
result = execGit(["diff", "HEAD"], { cwd: "." });
} else {
// get diff between branch base and HEAD
result = execGit(["diff", "origin/main...HEAD"], { cwd: "." });
}

if (result.trim()) {
const segments = [
...result.split("diff --git a/").filter((segment) => segment !== ""),
];
for (const segment of segments) {
const path = segment
.substring(0, segment.indexOf(" "))
.replaceAll(/files\/en-us\/|\/index.md/gm, "");

const headerRx = /^-#+ .*$/gm;
const fragments = [...segment.matchAll(headerRx)]
.map((match) => match[0].toLowerCase())
.map((header) => header.replace(/-#+ /g, ""))
.map((header) => stringToFragment(header));

for (const fragment of fragments) {
fragmentDetails.push(`${path}#${fragment}`);
}
}
}
console.log("fragmentDetails", fragmentDetails);
}

if (process.argv[2] !== "--workflow") {
getDeletedSlugs();
getFragmentDetails();
} else {
getFragmentDetails(false);
}

if (deletedSlugs.length < 1 && fragmentDetails.length < 1) {
console.log("Nothing to check. 🎉");
process.exit(0);
}

for await (const filePath of walkSync(getRootDir())) {
if (filePath.endsWith("index.md")) {
try {
const content = await fs.readFile(filePath, "utf-8");
const relativePath = filePath.substring(filePath.indexOf("files/en-us"));

// check deleted links
for (const slug of deletedSlugs) {
isAllOk = false;
const locations = getLocations(
content,
new RegExp(`/${slug}[)># \"']`, "mig"),
);
if (locations.length) {
for (const location of locations) {
console.error(
`ERROR:${relativePath}:${location.line}:${location.column}:Slug '${slug}' has been deleted`,
);
}
}
}

// check broken URL fragment
for (const fragment of fragmentDetails) {
isAllOk = false;
const locations = getLocations(content, fragment);
// check fragments in the same file
const urlParts = fragment.split("#");
if (filePath.includes(urlParts[0])) {
locations.push(...getLocations(content, urlParts[1]));
}
if (locations.length) {
for (const location of locations) {
console.error(
`ERROR:${relativePath}:${location.line}:${location.column}:URL fragment '${fragment}' is broken`,
);
}
}
}
} catch (e) {
console.error(`Error processing ${filePath}: ${e.message}`);
throw e;
}
}
}

if(!isAllOk) {
process.exit(1);
}
40 changes: 39 additions & 1 deletion scripts/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ 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 const IMG_RX = /(\.png|\.jpg|\.svg|\.gif)$/gim;

export async function* walkSync(dir) {
const files = await fs.readdir(dir, { withFileTypes: true });
Expand Down Expand Up @@ -49,3 +49,41 @@ export function getRootDir() {
export function isImagePath(path) {
return IMG_RX.test(path);
}

/*
* Returns locations (line and column numbers) of 'searchValue' in the given 'content'.
*/
export function getLocations(content, searchValue) {
const lineLengths = content.split("\n").map((line) => line.length);
const searchRx =
searchValue instanceof RegExp
? searchValue
: new RegExp(searchValue, "mig");
const matches = [...content.matchAll(searchRx)].map((match) => match.index);
const positions = [];

let currentPosition = 0;
lineLengths.forEach((lineLength, index) => {
lineLength += 1; // add '\n'
for (const match of matches) {
if (currentPosition < match && currentPosition + lineLength > match) {
positions.push({
line: index + 1,
column: match - currentPosition + 1,
});
}
}
currentPosition += lineLength;
});
return positions;
}

/*
* Convert Markdown header into URL slug.
*/
export function stringToFragment(text) {
return text
.trim()
.replace(/["#$%&+,/:;=?@[\]^`{|}~')(\\]/g, "")
.replace(/\s+/g, "_");
}
Loading