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

ci: check links validity #1

Merged
merged 3 commits into from
Nov 7, 2024
Merged
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
13 changes: 13 additions & 0 deletions .github/actions/reopen-issue-with-comment/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: "Reopen and comment"
description: "Reopen an issue if closed and add a comment"
inputs:
token:
description: "GITHUB_TOKEN or a repo scoped PAT."
default: ${{ github.token }}
issue-number:
description: "The number of the issue or pull request in which to create a comment."
comment:
description: "The comment body."
runs:
using: "node16"
main: "./main.js"
37 changes: 37 additions & 0 deletions .github/actions/reopen-issue-with-comment/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import * as core from "@actions/core";
import * as github from "@actions/github";

async function run() {
try {
const inputs = {
token: core.getInput("token"),
issueNumber: Number(core.getInput("issue-number")),
comment: core.getInput("comment"),
};

const repository = process.env.GITHUB_REPOSITORY;
const [owner, repo] = repository.split("/");

const octokit = github.getOctokit(inputs.token);

core.info("Re-opening the issue");
await octokit.rest.issues.update({
owner: owner,
repo: repo,
issue_number: inputs.issueNumber,
state: "open",
});

core.info("Adding a comment");
await octokit.rest.issues.createComment({
owner: owner,
repo: repo,
issue_number: inputs.issueNumber,
body: inputs.comment.replace(/<br \/>/g, `\n`),
});
} catch (error) {
core.setFailed(error.message);
}
}

run();
25 changes: 25 additions & 0 deletions .github/workflows/check-links-validity.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Check links validity
on:
schedule:
# https://crontab.guru/#0_11_*_*_2
- cron: "0 11 * * 2"
jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
submodules: "true"
- uses: actions/setup-node@v4
with:
cache: yarn
- run: yarn install --frozen-lockfile
- run: yarn run compile:rules
- id: invalid_links
run: node ./scripts/check-links-validity.js --ci
timeout-minutes: 15
- if: steps.invalid_links.outputs.comment
uses: ./.github/actions/reopen-issue-with-comment
with:
issue-number: 2
comment: ${{ steps.invalid_links.outputs.comment }}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
},
"devDependencies": {
"@changesets/cli": "^2.27.9",
"@actions/core": "^1.11.1",
"@actions/github": "^6.0.0",
"@etalab/decoupage-administratif": "^4.0.0",
"@publicodes/tools": "^1.3.0-1",
"@types/jest": "^29.5.13",
Expand Down
116 changes: 116 additions & 0 deletions scripts/check-links-validity.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import aidesVeloRules from "../publicodes-build/aides-velo.model.json" assert { type: "json" };

// cli param --grep to filter the links to check
const grepOptionIndex = process.argv
.slice(2)
.findIndex((arg) => arg.includes("--grep") || arg.includes("-g"));

const grepFilter =
grepOptionIndex !== -1 ? process.argv.slice(2)[grepOptionIndex + 1] : null;

// Extrait la liste des liens référencés dans la base de règles
const links = Object.entries(aidesVeloRules)
.reduce(
(acc, [, rule]) => [
...acc,
{ title: rule?.titre ?? null, link: rule?.lien ?? null },
],
[]
)
.filter(
({ link }) =>
link !== null && (grepFilter === null || link.includes(grepFilter))
);

// Certains sites référencés ont des problèmes de certificats, mais ce n'est pas
// ce que nous cherchons à détecter ici.
process.env.NODE_TLS_REJECT_UNAUTHORIZED = 0;

// Création d'une queue permettant de paralléliser la vérification des liens
const queue = [...links];
const detectedErrors = [];
const simultaneousItems = 5;

async function processNextQueueItem() {
if (queue.length !== 0) {
await fetchAndReport(queue.shift());
await processNextQueueItem();
}
}

async function fetchAndReport({ link, title }) {
let status = await getHTTPStatus(link);

// Retries in case of timeout
let remainingRetries = 3;
while (status === 499 && remainingRetries > 0) {
remainingRetries--;
await sleep(20_000);
status = await getHTTPStatus(link);
}
report({ status, link, title });
}

async function getHTTPStatus(link) {
const maxTime = 15_000;
const controller = new AbortController();
setTimeout(() => controller.abort(), maxTime);

try {
const res = await fetch(link, {
signal: controller.signal,
headers: {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.93 Safari/537.36",
},
});
return res.status;
} catch (err) {
return 499;
}
}

async function report({ status, link, title }) {
console.log(status === 200 ? "✅" : "❌", status, link);
if (status !== 200) {
detectedErrors.push({ status, link, title });
}
}

function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

(async () => {
await Promise.allSettled(
Array.from({ length: simultaneousItems }).map(processNextQueueItem)
);
if (detectedErrors.length > 0) {
// Formattage spécifique pour récupérer le résultat avec l'action Github
if (process.argv.slice(2).includes("--ci")) {
const message = `

Certains liens référencés ne semblent plus fonctionner :

| Aide | Status HTTP |
|---|---|
${detectedErrors
.map(({ status, title, link }) => `| [${title}](${link}) | ${status} |`)
.join("\n")}`;

const format = (msg) =>
msg
.trim()
.split("\n")
.map((line) => line.trim())
.join("<br />");
console.log(`::set-output name=comment::${format(message)}`);
} else if (detectedErrors) {
console.log(
"Liens invalides :" + detectedErrors.map(({ link }) => `\n- ${link}`)
);
}

console.log("Terminé");
}
})();
Loading