Skip to content

Commit 1a0dd83

Browse files
ckohenJ-HumaniCrawlIanMitchellalmeidx
authored
Add Link Checking Capability and CI (#3930)
* Add link check CI * Fix Links * remove extra space Co-authored-by: J-Human <[email protected]> * Lint json, use node prefix Co-Authored-By: Noel <[email protected]> * json prettier part 2 electric boogaloo Co-Authored-By: Noel <[email protected]> * More efficient replacement in anchor detection * Use regex for heading level check * General refactor and cleanup Co-Authored-By: Ian Mitchell <[email protected]> Co-Authored-By: Almeida <[email protected]> Co-authored-by: J-Human <[email protected]> Co-authored-by: Noel <[email protected]> Co-authored-by: Ian Mitchell <[email protected]> Co-authored-by: Almeida <[email protected]>
1 parent 54a0f48 commit 1a0dd83

33 files changed

+3715
-236
lines changed

.eslintrc.json

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"extends": "marine/prettier/node",
3+
"ignorePatterns": ["dist/*", "**/*.js"],
4+
"parserOptions": {
5+
"project": "./tsconfig.eslint.json"
6+
},
7+
"rules": {
8+
"no-eq-null": "off"
9+
}
10+
}

.github/workflows/test.yaml

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: Testing
2+
on: [push, pull_request]
3+
jobs:
4+
lint:
5+
name: ESLint
6+
runs-on: ubuntu-latest
7+
steps:
8+
- name: Checkout repository
9+
uses: actions/checkout@v2
10+
11+
- name: Install Node v16
12+
uses: actions/setup-node@v2
13+
with:
14+
node-version: 16
15+
cache: npm
16+
17+
- name: Install dependencies
18+
run: npm ci
19+
20+
- name: Run ESLint
21+
run: npm run lint
22+
23+
links:
24+
name: Check Links
25+
runs-on: ubuntu-latest
26+
steps:
27+
- name: Checkout repository
28+
uses: actions/checkout@v2
29+
30+
- name: Install Node v16
31+
uses: actions/setup-node@v2
32+
with:
33+
node-version: 16
34+
cache: npm
35+
36+
- name: Install dependencies
37+
run: npm ci
38+
39+
- name: Build
40+
run: npm run build
41+
42+
- name: Run Link Checks
43+
run: npm run test:links

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
.DS_Store
22
*.swp
3+
node_modules/
4+
dist/

.prettierignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
docs/

.prettierrc.json

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"quoteProps": "consistent",
3+
"endOfLine": "lf",
4+
"printWidth": 120,
5+
"useTabs": true,
6+
"overrides": [
7+
{
8+
"files": "*.md",
9+
"options": {
10+
"useTabs": false,
11+
"tabWidth": 4
12+
}
13+
}
14+
]
15+
}

ci/checkLinks.ts

+175
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import { readdirSync, statSync, readFileSync } from "node:fs";
2+
import path from "node:path";
3+
import chalk from "chalk";
4+
import * as github from "@actions/core";
5+
const cwd = process.env.GITHUB_ACTIONS ? process.env.GITHUB_WORKSPACE! : process.cwd();
6+
7+
function importDirectory(directory: string, extension: string, subdirectories = true) {
8+
try {
9+
const output = new Map<string, string>();
10+
const files = readdirSync(directory);
11+
for (const fileOrPath of files) {
12+
const currentPath = path.join(directory, fileOrPath);
13+
if (statSync(currentPath).isDirectory()) {
14+
if (!subdirectories) continue;
15+
const subdir = importDirectory(currentPath, extension, subdirectories);
16+
if (!subdir) continue;
17+
for (const [name, read] of subdir) {
18+
output.set(`/${fileOrPath}${name}`, read);
19+
}
20+
continue;
21+
}
22+
if (!fileOrPath.endsWith(extension)) continue;
23+
const read = readFileSync(currentPath, "utf8");
24+
output.set(`/${fileOrPath}`, read);
25+
}
26+
return output;
27+
} catch {
28+
// Directory likely does not exist, we should be able to safely discard this error
29+
return null;
30+
}
31+
}
32+
33+
function printResults(resultMap: Map<string, github.AnnotationProperties[]>): void {
34+
let output = "\n";
35+
let total = 0;
36+
for (const [resultFile, resultArr] of resultMap) {
37+
if (resultArr.length <= 0) continue;
38+
const filePath = path.join(cwd, resultFile);
39+
output += `${chalk.underline(filePath)}\n`;
40+
output += resultArr.reduce<string>((result, props) => {
41+
total += 1;
42+
return `${result} ${props.startLine ?? ""}:${props.startColumn ?? ""}-${props.endColumn ?? ""} ${chalk.yellow(
43+
"warning"
44+
)} ${props.title ?? ""}\n`;
45+
}, "");
46+
output += "\n";
47+
}
48+
output += "\n";
49+
if (total > 0) {
50+
output += chalk.red.bold(`\u2716 ${total} problem${total === 1 ? "" : "s"}\n`);
51+
}
52+
console.log(output);
53+
}
54+
55+
function annotateResults(resultMap: Map<string, github.AnnotationProperties[]>): void {
56+
let total = 0;
57+
for (const [resultFile, resultArr] of resultMap) {
58+
if (resultArr.length <= 0) continue;
59+
github.startGroup(resultFile);
60+
for (const result of resultArr) {
61+
total += 1;
62+
console.log(
63+
`::warning file=${resultFile},title=Invalid Link,line=${result.startLine ?? 0},endLine=${
64+
result.startLine ?? 0
65+
},col=${result.startColumn ?? 0},endColumn=${result.endColumn ?? result.startColumn ?? 0}::${
66+
result.title ?? "Invalid Link"
67+
}`
68+
);
69+
}
70+
github.endGroup();
71+
}
72+
if (total > 0) {
73+
github.setFailed("One or more links are invalid!");
74+
}
75+
}
76+
77+
const docFiles = importDirectory(path.join(cwd, "docs"), ".md");
78+
79+
if (!docFiles) {
80+
console.error("No doc files found!");
81+
process.exit(1);
82+
}
83+
84+
const validLinks = new Map<string, string[]>([
85+
["APPLICATIONS", []],
86+
["SERVERS", []],
87+
["TEAMS", []],
88+
]);
89+
90+
const extLength = ".md".length;
91+
92+
// Gather valid links
93+
for (const [name, raw] of docFiles) {
94+
const keyName = `DOCS${name.slice(0, -extLength).replaceAll("/", "_").toUpperCase()}`;
95+
if (!validLinks.has(keyName)) {
96+
validLinks.set(keyName, []);
97+
}
98+
const validAnchors = validLinks.get(keyName)!;
99+
100+
let parentAnchor = "";
101+
let multilineCode = false;
102+
for (const line of raw.split("\n")) {
103+
if (line.trim().startsWith("```")) {
104+
multilineCode = !multilineCode;
105+
if (line.trim().length > 3 && line.trim().endsWith("```")) multilineCode = !multilineCode;
106+
}
107+
if (multilineCode || !line.startsWith("#")) continue;
108+
const anchor = line
109+
.split("%")[0]
110+
.replace(/[^ A-Z0-9]/gi, "")
111+
.trim()
112+
.replace(/ +/g, "-")
113+
.toLowerCase();
114+
if (/^#{1,4}(?!#)/.test(line.trim())) {
115+
parentAnchor = `${anchor}-`;
116+
validAnchors.push(anchor);
117+
continue;
118+
}
119+
validAnchors.push(`${parentAnchor}${anchor}`);
120+
}
121+
}
122+
123+
const results = new Map<string, github.AnnotationProperties[]>();
124+
125+
// Check Links
126+
for (const [name, raw] of docFiles) {
127+
const fileName = `docs${name}`;
128+
const file = raw.split("\n");
129+
if (!results.has(fileName)) {
130+
results.set(fileName, []);
131+
}
132+
const ownResults = results.get(fileName)!;
133+
let multilineCode = false;
134+
file.forEach((line, lineNum) => {
135+
if (line.trim().startsWith("```")) {
136+
multilineCode = !multilineCode;
137+
if (line.trim().length > 3 && line.trim().endsWith("```")) multilineCode = !multilineCode;
138+
}
139+
if (multilineCode) return;
140+
const matches = line.matchAll(/(?<![!`])\[.+?\]\((?!https?|mailto)(.+?)\)(?!`)/g);
141+
142+
for (const match of matches) {
143+
const split = match[1].split("#")[1].split("/");
144+
const page = split[0];
145+
const anchor = split[1];
146+
if (!validLinks.has(page)) {
147+
ownResults.push({
148+
title: `Base url ${chalk.blueBright(page)} does not exist`,
149+
startLine: lineNum + 1,
150+
startColumn: match.index,
151+
endColumn: (match.index ?? 0) + match[0].length,
152+
});
153+
continue;
154+
}
155+
156+
if (!anchor) continue;
157+
if (!validLinks.get(page)!.includes(anchor)) {
158+
ownResults.push({
159+
title: `Anchor ${chalk.cyan(anchor)} does not exist on ${chalk.blueBright(page)}`,
160+
startLine: lineNum + 1,
161+
startColumn: match.index,
162+
endColumn: (match.index ?? 0) + match[0].length,
163+
});
164+
}
165+
}
166+
});
167+
}
168+
169+
if (results.size > 0) {
170+
if (process.env.GITHUB_ACTIONS) {
171+
annotateResults(results);
172+
} else {
173+
printResults(results);
174+
}
175+
}

docs/Change_Log.md

+11-11
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,9 @@ You can enable or disable a command (guild or global) for a specific user or rol
7171

7272
New routes have been added to support this functionality:
7373

74-
- [`GET Guild Application Command Permissions`](#DOCS_INTERACTIONS_APPLICATION_COMMANDS/getguildapplicationcommandpermissions)
75-
- [`GET Application Command Permissions`](#DOCS_INTERACTIONS_APPLICATION_COMMANDS/getapplicationcommandpermissions)
76-
- [`PUT Application Command Permissions`](#DOCS_INTERACTIONS_APPLICATION_COMMANDS/putapplicationcommandpermissions)
74+
- [`GET Guild Application Command Permissions`](#DOCS_INTERACTIONS_APPLICATION_COMMANDS/get-guild-application-command-permissions)
75+
- [`GET Application Command Permissions`](#DOCS_INTERACTIONS_APPLICATION_COMMANDS/get-application-command-permissions)
76+
- [`PUT Application Command Permissions`](#DOCS_INTERACTIONS_APPLICATION_COMMANDS/batch-edit-application-command-permissions)
7777

7878
A `default_permission` field has also been added to the [ApplicationCommand](#DOCS_INTERACTIONS_APPLICATION_COMMANDS/application-command-object-application-command-structure) model. This field allows you to disable commands for everyone in a guild by default, if you prefer to make some of your commands an opt-in experience.
7979

@@ -268,7 +268,7 @@ Fixed a bug from the 2.5.5 release that caused network handshakes to fail, resul
268268

269269
#### November 14, 2019
270270

271-
We've shipped some updates to the GameSDK, including support for Linux as well as the IL2CPP backend system for Unity. These changes also fixed a bug in the [`SetUserAchievement()`](#DOCS_GAME_SDK_ACHIEVEMENTS/set-user-achievement) method.
271+
We've shipped some updates to the GameSDK, including support for Linux as well as the IL2CPP backend system for Unity. These changes also fixed a bug in the [`SetUserAchievement()`](#DOCS_GAME_SDK_ACHIEVEMENTS/setuserachievement) method.
272272

273273
Get the latest at the top of the [Getting Started](#DOCS_GAME_SDK_SDK_STARTER_GUIDE/step-1-get-the-thing) documentation. If you're looking for help interacting with the GameSDK or want to report a bug, join us on the [official Discord](https://discord.gg/discord-developers).
274274

@@ -282,7 +282,7 @@ News Channels are now changed to [Announcement Channels](#DOCS_GAME_AND_SERVER_M
282282

283283
#### August 12, 2019
284284

285-
You can now get more precise rate limit reset times, via a new request header. Check out the [rate limits](#DOCS_TOPICS_RATE_LIMITS/more-precise-rate-limit-resets) documentation for more information.
285+
You can now get more precise rate limit reset times, via a new request header. Check out the [rate limits](#DOCS_TOPICS_RATE_LIMITS/) documentation for more information.
286286

287287
## Bot Tokens for Achievements
288288

@@ -294,7 +294,7 @@ You can now use Bot tokens for authorization headers against the HTTP API for [A
294294

295295
#### June 19, 2019
296296

297-
Additional information around Teams has been added to both the API and the documentation. The [Teams](#DOCS_TOPICS_TEAMS/api-stuff) page now includes information about the team and team member objects. Additionally, the [Get Current Application Information](#DOCS_TOPICS_OAUTH2/get-current-application-information) endpoint now returns a `team` object if that application belongs to a team. That documentation has also been updated to includes fields that were missing for applications that are games sold on Discord.
297+
Additional information around Teams has been added to both the API and the documentation. The [Teams](#DOCS_TOPICS_TEAMS/teams) page now includes information about the team and team member objects. Additionally, the [Get Current Application Information](#DOCS_TOPICS_OAUTH2/get-current-bot-application-information) endpoint now returns a `team` object if that application belongs to a team. That documentation has also been updated to includes fields that were missing for applications that are games sold on Discord.
298298

299299
## Added Info Around Nitro Boosting Experiment
300300

@@ -362,7 +362,7 @@ The [Get Guild Emoji](#DOCS_RESOURCES_EMOJI/get-guild-emoji) response now also i
362362

363363
#### January 23, 2018
364364

365-
The [Accept Invite](#DOCS_RESOURCES_INVITE/accept-invite) endpoint is deprecated starting today, and will be discontinued on March 23, 2018. The [Add Guild Member](#DOCS_RESOURCES_GUILD/add-guild-member) endpoint should be used in its place.
365+
The [Accept Invite](#DOCS_RESOURCES_INVITE/) endpoint is deprecated starting today, and will be discontinued on March 23, 2018. The [Add Guild Member](#DOCS_RESOURCES_GUILD/add-guild-member) endpoint should be used in its place.
366366

367367
## Semi-Breaking Change: Very Large Bot Sharding
368368

@@ -386,7 +386,7 @@ For more information, check out our [Rich Presence site](https://discord.com/ric
386386

387387
#### October 16, 2017
388388

389-
[API](#DOCS_REFERENCE/api-versioning) and [Gateway](#DOCS_TOPICS_GATEWAY/gateway-protocol-versions) versions below v6 are now discontinued after being previously deprecated. Version 6 is now the default API and Gateway version. Attempting to use a version below 6 will result in an error.
389+
[API](#DOCS_REFERENCE/api-versioning) and [Gateway](#DOCS_TOPICS_GATEWAY/gateways-gateway-versions) versions below v6 are now discontinued after being previously deprecated. Version 6 is now the default API and Gateway version. Attempting to use a version below 6 will result in an error.
390390

391391
## New Feature: Channel Categories
392392

@@ -404,7 +404,7 @@ Changes have been made throughout the documentation to reflect the addition of c
404404

405405
#### August 16, 2017
406406

407-
The `type` field in the [activity object](#DOCS_TOPICS_GATEWAY/activity-object) for [Gateway Status Update](#DOCS_TOPICS_GATEWAY/update-status) and [Presence Update](#DOCS_TOPICS_GATEWAY/presence-update) payloads is no longer optional when the activity object is not null.
407+
The `type` field in the [activity object](#DOCS_TOPICS_GATEWAY/activity-object) for [Gateway Status Update](#DOCS_TOPICS_GATEWAY/update-presence) and [Presence Update](#DOCS_TOPICS_GATEWAY/presence-update) payloads is no longer optional when the activity object is not null.
408408

409409
## Breaking Change: Default Channels
410410

@@ -420,7 +420,7 @@ We are also rolling out a change in conjunction that will allow Discord to remem
420420

421421
#### July 24, 2017
422422

423-
Audit logs are here! Well, they've been here all along, but now we've got [documentation](#DOCS_AUDIT_LOG/audit-logs) about them. Check it out, but remember: with great power comes great responsibility.
423+
Audit logs are here! Well, they've been here all along, but now we've got [documentation](#DOCS_RESOURCES_AUDIT_LOG/) about them. Check it out, but remember: with great power comes great responsibility.
424424

425425
## Breaking Change: Version 6
426426

@@ -432,5 +432,5 @@ Audit logs are here! Well, they've been here all along, but now we've got [docum
432432
- `recipient` is now `recipients`, an array of [user](#DOCS_RESOURCES_USER/user-object) objects
433433
- [Message](#DOCS_RESOURCES_CHANNEL/message-object) Object
434434
- [`type`](#DOCS_RESOURCES_CHANNEL/message-object-message-types) added to support system messages
435-
- [Status Update](#DOCS_TOPICS_GATEWAY/update-status-gateway-status-update-structure) Object
435+
- [Status Update](#DOCS_TOPICS_GATEWAY/update-presence-gateway-presence-update-structure) Object
436436
- `idle_since` renamed to `since`

docs/Reference.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ In API v8, we've improved error formatting in form error responses. The response
102102

103103
Authenticating with the Discord API can be done in one of two ways:
104104

105-
1. Using a bot token gained by [registering a bot](#DOCS_TOPICS_OAUTH2/registering-applications), for more information on bots see [bots vs user accounts](#DOCS_TOPICS_OAUTH2/bot-vs-user-accounts).
105+
1. Using a bot token gained by [registering a bot](#APPLICATIONS), for more information on bots see [bots vs user accounts](#DOCS_TOPICS_OAUTH2/bot-vs-user-accounts).
106106
2. Using an OAuth2 bearer token gained through the [OAuth2 API](#DOCS_TOPICS_OAUTH2/oauth2).
107107

108108
For all authentication types, authentication is performed with the `Authorization` HTTP header in the format `Authorization: TOKEN_TYPE TOKEN`.
@@ -317,7 +317,7 @@ Discord uses ids and hashes to render images in the client. These hashes can be
317317
| Application Asset | app-assets/[application_id](#DOCS_RESOURCES_APPLICATION/application-object)/[asset_id](#DOCS_TOPICS_GATEWAY/activity-object-activity-assets).png | PNG, JPEG, WebP |
318318
| Achievement Icon | app-assets/[application_id](#DOCS_RESOURCES_APPLICATION/application-object)/achievements/[achievement_id](#DOCS_GAME_SDK_ACHIEVEMENTS/data-models-user-achievement-struct)/icons/[icon_hash](#DOCS_GAME_SDK_ACHIEVEMENTS/data-models-user-achievement-struct).png | PNG, JPEG, WebP |
319319
| Sticker Pack Banner | app-assets/710982414301790216/store/[sticker_pack_banner_asset_id](#DOCS_RESOURCES_STICKER/sticker-pack-object).png | PNG, JPEG, WebP |
320-
| Team Icon | team-icons/[team_id](#DOCS_TOPICS_TEAMS/team-object)/[team_icon](#DOCS_TOPICS_TEAMS/team-object).png | PNG, JPEG, WebP |
320+
| Team Icon | team-icons/[team_id](#DOCS_TOPICS_TEAMS/data-models-team-object)/[team_icon](#DOCS_TOPICS_TEAMS/data-models-team-object).png | PNG, JPEG, WebP |
321321
| Sticker | stickers/[sticker_id](#DOCS_RESOURCES_STICKER/sticker-object).png \*\*\* \*\*\*\* | PNG, Lottie |
322322
| Role Icon | role-icons/[role_id](#DOCS_TOPICS_PERMISSIONS/role-object)/[role_icon](#DOCS_TOPICS_PERMISSIONS/role-object).png \* | PNG, JPEG, WebP |
323323

docs/dispatch/List_of_Commands.md

+6-6
Original file line numberDiff line numberDiff line change
@@ -234,12 +234,12 @@ Displays a preview of the install paths that a build will put files in, for a gi
234234

235235
###### Arguments
236236

237-
| name | values | description |
238-
| ---------------- | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
239-
| config_file | filename | the [JSON config file](#DOCS_DISPATCH_BRANCHES_AND_BUILDS/setting-up-our-first-build) for the build |
240-
| application_root | file path | the directory that dispatch will treat as the local root for operations—`.` for the current directory |
241-
| --locale | [locale](#DOCS_DISPATCH_FIELD_VALES/predefined-field-values-accepted-locales) | the build locale to preview |
242-
| --platform | [platform](#DOCS_DISPATCH_FIELD_VALUES/manifest-platform-values) | the build platform to preview |
237+
| name | values | description |
238+
| ---------------- | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- |
239+
| config_file | filename | the [JSON config file](#DOCS_DISPATCH_BRANCHES_AND_BUILDS/setting-up-our-first-build) for the build |
240+
| application_root | file path | the directory that dispatch will treat as the local root for operations—`.` for the current directory |
241+
| --locale | [locale](#DOCS_DISPATCH_FIELD_VALUES/predefined-field-values-accepted-locales) | the build locale to preview |
242+
| --platform | [platform](#DOCS_DISPATCH_FIELD_VALUES/manifests-platform-values) | the build platform to preview |
243243

244244
###### Example
245245

0 commit comments

Comments
 (0)