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

Make changes to check for broken links #4

Merged
merged 5 commits into from
Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
36 changes: 36 additions & 0 deletions .github/workflows/check.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Check links

on:
# when someone makes a change directly to main branch
push:
branches:
- main
# when someone requests a change to main branch
pull_request:
branches:
- main

# run periodically
schedule:
- cron: "0 0 * * *"
# run manually
workflow_dispatch:

jobs:
check:
runs-on: ubuntu-latest
steps:
- if: runner.debug == '1'
uses: mxschmitt/action-tmate@v3

- name: Get this repo's code
uses: actions/checkout@v4

- name: Set up Bun
uses: oven-sh/setup-bun@v1

- name: Install packages
run: bun install glob@v9 yaml@v2

- name: Run check script
run: bun ./check.js
15 changes: 10 additions & 5 deletions .github/workflows/deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,30 @@ env:

jobs:
encode:
name: Encode and deploy
runs-on: ubuntu-latest
steps:
- if: runner.debug == '1'
uses: mxschmitt/action-tmate@v3

- name: Get this repo's code
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
path: redirects-repo # save in separate sub-folder

- name: Get website repo's code
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
repository: ${{ github.repository_owner }}/${{ env.website_repo }} # assume same user/org
path: website-repo # save in separate sub-folder

- name: Set up Bun
uses: oven-sh/setup-bun@v1

- name: Install packages
run: npm install glob@v9 yaml@v2
vincerubinetti marked this conversation as resolved.
Show resolved Hide resolved
run: bun install glob@v9 yaml@v2

- name: Run encode script
run: node ./redirects-repo/encode.js
run: bun ./redirects-repo/encode.js

- name: Commit result to website repo
if: ${{ github.event_name == 'push' }}
Expand Down
20 changes: 20 additions & 0 deletions check.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { addError, list } from "./core";

// for each redirect
await Promise.all(
list.map(async ({ to }) => {
try {
// do simple request to target url
const response = await fetch(to);
if (
// only fail on certain status codes that might indicate link is "broken"
[
400, 404, 405, 406, 408, 409, 410, 421, 500, 501, 502, 503, 504,
].includes(response.status)
)
throw Error(response.status);
} catch (error) {
addError(`"to: ${to}" may be a broken link\n(${error})`);
}
})
);
118 changes: 118 additions & 0 deletions core.js
vincerubinetti marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { readFileSync } from "fs";
import { resolve } from "path";
import { globSync } from "glob";
import { parse } from "yaml";

// collect (caught) errors to report at end
const errors = [];

// if running in github actions debug mode, do extra logging
export const verbose = !!process.env.RUNNER_DEBUG;

// get yaml files that match glob pattern
const files = globSync("*.y?(a)ml", { cwd: __dirname });

log("Files", files.join(" "));

// start combined list of redirects
export const list = [];

// keep track of duplicate entries
const duplicates = {};

// go through each yaml file
for (const file of files) {
// load file contents
const contents = readFileSync(resolve(__dirname, file), "utf8");

// try to parse as yaml
let data;
try {
data = parse(contents);
} catch (error) {
addError(`Couldn't parse ${file}. Make sure it is valid YAML.`);
continue;
}

// check if top level is list
if (!Array.isArray(data)) {
addError(`${file} is not a list`);
continue;
}

// go through each entry
for (let [index, entry] of Object.entries(data)) {
index = Number(index) + 1;
const trace = `${file} entry ${index}`;

// check if dict
if (typeof entry !== "object") {
addError(`${trace} is not a dict`);
continue;
}

// check "from" field
if (!(typeof entry.from === "string" && entry.from.trim())) {
addError(`${trace} "from" field invalid`);
continue;
}

// check "to" field
if (!(typeof entry.to === "string" && entry.to.trim()))
addError(`${trace} "to" field invalid`);

// normalize "from" field. lower case, remove leading slashes.
entry.from = entry.from.toLowerCase().replace(/^(\/+)/, "");

// add to combined list
list.push(entry);

// add to duplicate list. record source file and entry number for logging.
duplicates[entry.from] ??= [];
duplicates[entry.from].push({ ...entry, file, index });
}
}

// check that any redirects exist
if (!list.length) addError("No redirects");

if (verbose) log("Combined redirects list", list);

// trigger errors for duplicates
for (const [from, entries] of Object.entries(duplicates)) {
const count = entries.length;
if (count <= 1) continue;
const duplicates = entries
.map(({ file, index }) => `\n ${file} entry ${index}`)
.join("");
addError(`"from: ${from}" appears ${count} time(s): ${duplicates}`);
}

// add error
export function addError(error) {
errors.push(error);
}

// when script finished
process.on("exit", () => {
// report all errors together
if (errors.length) {
process.exitCode = 1;
errors.forEach(logError);
logError(`${errors.length} error(s)`);
} else {
process.exitCode = 0;
log("No errors!");
}
});

// formatted normal log
export function log(message, data) {
console.info("\x1b[1m\x1b[96m" + message + "\x1b[0m");
if (data) console.log(data);
}

// formatted error log
export function logError(message) {
console.error("\x1b[1m\x1b[91m" + message + "\x1b[0m");
}
122 changes: 13 additions & 109 deletions encode.js
Original file line number Diff line number Diff line change
@@ -1,98 +1,5 @@
const { readFileSync, writeFileSync } = require("fs");
const { resolve } = require("path");
const { globSync } = require("glob");
const { parse } = require("yaml");

// collect (caught) errors to report at end
const errors = [];

// report errors on exit
process.on("exit", () => {
errors.forEach(error);
if (errors.length) error(`${errors.length} error(s)`);
});

// if running in github actions debug mode, do extra logging
const verbose = !!process.env.RUNNER_DEBUG;

// get yaml files that match glob pattern
const files = globSync("*.y?(a)ml", { cwd: __dirname });

log("Files", files.join(" "));

// start combined list of redirects
const list = [];

// keep track of duplicate entries
const duplicates = {};

// go through each yaml file
for (const file of files) {
// load file contents
const contents = readFileSync(resolve(__dirname, file), "utf8");

// try to parse as yaml
let data;
try {
data = parse(contents);
} catch (error) {
errors.push(`Couldn't parse ${file}. Make sure it is valid YAML.`);
continue;
}

// check if top level is list
if (!Array.isArray(data)) {
errors.push(`${file} is not a list`);
continue;
}

// go through each entry
for (let [index, entry] of Object.entries(data)) {
index = Number(index) + 1;
const trace = `${file} entry ${index}`;

// check if dict
if (typeof entry !== "object") {
errors.push(`${trace} is not a dict`);
continue;
}

// check "from" field
if (!(typeof entry.from === "string" && entry.from.trim())) {
errors.push(`${trace} "from" field invalid`);
continue;
}

// check "to" field
if (!(typeof entry.to === "string" && entry.to.trim()))
errors.push(`${trace} "to" field invalid`);

// normalize "from" field. lower case, remove leading slashes.
entry.from = entry.from.toLowerCase().replace(/^(\/+)/, "");

// add to combined list
list.push(entry);

// add to duplicate list. record source file and entry number for logging.
duplicates[entry.from] ??= [];
duplicates[entry.from].push({ ...entry, file, index });
}
}

// check that any redirects exist
if (!list.length) errors.push("No redirects");

if (verbose) log("Combined redirects list", list);

// trigger errors for duplicates
for (const [from, entries] of Object.entries(duplicates)) {
const count = entries.length;
if (count <= 1) continue;
const duplicates = entries
.map(({ file, index }) => `\n ${file} entry ${index}`)
.join("");
errors.push(`"from: ${from}" appears ${count} time(s): ${duplicates}`);
}
import { readFileSync, writeFileSync } from "fs";
import { addError, list, verbose } from "./core";

// encode redirect list to base64 to obfuscate
const encoded = Buffer.from(JSON.stringify(list)).toString("base64");
Expand All @@ -103,7 +10,12 @@ if (verbose) log("Encoded redirects list", encoded);
const script = "./website-repo/redirect.js";

// load contents of script
const contents = readFileSync(script, "utf8").toString();
let contents = "";
try {
contents = readFileSync(script, "utf8").toString();
} catch (error) {
addError(`Couldn't find script file at ${script}`);
}

// pattern to extract encoded redirect list from script string
const regex = /(list\s*=\s*")([A-Za-z0-9+\/=]*)(")/;
Expand All @@ -115,22 +27,14 @@ if (verbose) log("Old encoded redirects list", oldEncoded);

// check that we could find it (and thus can replace it)
if (typeof oldEncoded !== "string")
errors.push("Couldn't find encoded redirects list in redirect script");
addError("Couldn't find encoded redirects list in redirect script");

// update encoded string in script
const newContents = contents.replace(regex, "$1" + encoded + "$3");

// write updated redirect script to website repo
writeFileSync(script, newContents, "utf-8");

// exit if collected errors
if (errors.length) process.exit(1);

// debug util
function log(message, data) {
console.info("\033[1m\033[96m" + message + "\033[0m");
console.log(data);
}
function error(message) {
console.error("\033[1m\033[91m" + message + "\033[0m");
try {
writeFileSync(script, newContents, "utf-8");
} catch (error) {
addError(`Couldn't write script file to ${script}`);
}