zola-check-manager

A Github Action allowing you to manage the zola check results in github
git clone git://archive.git.mtrnord.blog/MTRNord/zola-check-manager.git
Log | Files | Refs | README | LICENSE

main.ts (8045B)


      1 import {addPath, debug, getInput, setFailed, summary} from '@actions/core';
      2 import {ExecOptions, exec} from '@actions/exec';
      3 import {getOctokit, context} from '@actions/github';
      4 import {which} from '@actions/io';
      5 import {cacheDir, downloadTool, extractTar, find} from '@actions/tool-cache';
      6 import {readFile} from 'fs/promises';
      7 import path from 'path';
      8 import grammar from './grammar.js';
      9 import got from 'got';
     10 // @ts-ignore
     11 import nearly from 'nearley/lib/nearley.js';
     12 const {Grammar, Parser} = nearly;
     13 
     14 interface WaybackResponse {
     15   archived_snapshots: {
     16     closest?: {
     17       available: boolean;
     18       url: string;
     19       timestamp: string;
     20       status: string;
     21     };
     22   };
     23 }
     24 
     25 // Purely used for tests
     26 function areWeTestingWithJest() {
     27   return process.env.JEST_WORKER_ID !== undefined;
     28 }
     29 
     30 async function downloadRelease(version: string): Promise<string> {
     31   // Download
     32   const downloadUrl = `https://github.com/getzola/zola/releases/download/v${version}/zola-v${version}-x86_64-unknown-linux-gnu.tar.gz`;
     33   let downloadPath: string | null = null;
     34   try {
     35     downloadPath = await downloadTool(downloadUrl);
     36   } catch (error) {
     37     debug(error as string);
     38     throw new Error(
     39       `Failed to download version v${version}: ${error as string}`
     40     );
     41   }
     42 
     43   // Extract
     44   const extPath = await extractTar(downloadPath);
     45 
     46   // Install into the local tool cache - node extracts with a root folder that matches the fileName downloaded
     47   return await cacheDir(extPath, 'zola', version);
     48 }
     49 
     50 async function getZolaCli(version: string): Promise<void> {
     51   // look if the binary is cached
     52   let toolPath: string;
     53   toolPath = find('zola', version);
     54 
     55   // if not: download, extract and cache
     56   if (!toolPath) {
     57     toolPath = await downloadRelease(version);
     58     debug(`Zola cached under ${toolPath}`);
     59   }
     60 
     61   addPath(toolPath);
     62 }
     63 
     64 export async function run(): Promise<void> {
     65   // __dirname does not exist in esm world so we fake it the esm way. Nodejs approves.
     66   const __dirname = process.env['GITHUB_WORKSPACE'] || '.';
     67 
     68   const working_directory = getInput('working_directory');
     69 
     70   let dataString = '';
     71   let infoString = '';
     72   const parser: typeof Parser = new Parser(Grammar.fromCompiled(grammar));
     73   const options: ExecOptions = {
     74     cwd: path.join(__dirname, working_directory),
     75     ignoreReturnCode: true,
     76     listeners: {
     77       stderr: (data: Buffer) => {
     78         dataString += data.toString();
     79       },
     80       stdout: (data: Buffer) => {
     81         infoString += data.toString();
     82       }
     83     }
     84   };
     85 
     86   // Download zola
     87   await getZolaCli('0.17.2');
     88 
     89   const zolaPath: string = await which('zola', true);
     90   const startTime = new Date();
     91 
     92   await exec(`${zolaPath}`, ['check'], options);
     93 
     94   try {
     95     parser.feed(dataString);
     96   } catch (parseError: unknown) {
     97     setFailed(`Error at character ${(parseError as {offset: string}).offset}`);
     98   }
     99 
    100   const annotations: {
    101     path: string;
    102     start_line: number;
    103     end_line: number;
    104     start_column: number;
    105     end_column: number;
    106     annotation_level: string;
    107     message: string;
    108   }[] = [];
    109 
    110   for (const rawResult of parser.results[0]) {
    111     const result = rawResult as {
    112       error_message: string;
    113       file?: string;
    114       url?: string;
    115     };
    116     if (!result.hasOwnProperty('file')) {
    117       continue;
    118     }
    119 
    120     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- We ensured this exists previously
    121     const data: string = await readFile(result.file!, 'utf8');
    122     const lines: string[] = data.split(/\r?\n/);
    123 
    124     for (const [index, line] of lines.entries()) {
    125       if (line.trim() === '') {
    126         continue;
    127       }
    128 
    129       const startingPositionOfUrl = line.indexOf(result.url ?? '');
    130 
    131       if (startingPositionOfUrl === -1) {
    132         continue;
    133       }
    134 
    135       let message = `Zola Error Message: ${result.error_message}`;
    136 
    137       // Check if we have a webarchive link
    138       const waybackResponse = await got
    139         .get(`http://archive.org/wayback/available?url=${result.url ?? ''}`)
    140         .json<WaybackResponse>();
    141       if (waybackResponse.archived_snapshots !== null) {
    142         if (
    143           waybackResponse.archived_snapshots.closest?.available &&
    144           waybackResponse.archived_snapshots.closest.status === '200'
    145         ) {
    146           message = `${message}\nWayback Machine Link is available: ${waybackResponse.archived_snapshots.closest.url}`;
    147         }
    148       }
    149 
    150       annotations.push({
    151         // This is a little awkward but does the job
    152         path: `/${path.relative(__dirname, result.file ?? '')}`,
    153         start_line: index,
    154         end_line: index,
    155         start_column: startingPositionOfUrl,
    156         end_column: startingPositionOfUrl + (result.url ?? '').length,
    157         annotation_level: getInput('annotation_level'),
    158         message
    159       });
    160     }
    161   }
    162 
    163   // Only create result if there is anything to report
    164   if (annotations.length > 0) {
    165     const token = getInput('repo-token');
    166     const octokit = getOctokit(token);
    167 
    168     // call octokit to create a check with annotation and details
    169     await octokit.rest.checks.create({
    170       owner: context.repo.owner,
    171       repo: context.repo.repo,
    172       name: 'Zola Check',
    173       head_sha: context.sha,
    174       started_at: areWeTestingWithJest() ? undefined : startTime.toISOString(),
    175       completed_at: areWeTestingWithJest()
    176         ? undefined
    177         : new Date().toISOString(),
    178       status: 'completed',
    179       conclusion: getInput('conclusion_level'),
    180       output: {
    181         title: 'Link is not reachable',
    182         summary:
    183           'Zola check found links which are not reachable. Make sure to either ignore these due to being false positives or fixing them',
    184         annotations
    185       }
    186     });
    187   }
    188 
    189   // Write summary
    190   const stdoutParser: typeof Parser = new Parser(Grammar.fromCompiled(grammar));
    191   stdoutParser.feed(infoString);
    192 
    193   if (
    194     // eslint-disable-next-line @typescript-eslint/no-explicit-any
    195     stdoutParser.results[0].filter((result: any) =>
    196       result.hasOwnProperty('successReport')
    197     ).length > 0
    198   ) {
    199     // eslint-disable-next-line @typescript-eslint/no-explicit-any
    200     const totalExternal = stdoutParser.results[0].filter((result: any) =>
    201       result.hasOwnProperty('external_links_planed_checking')
    202     )[0]['external_links_planed_checking']['total'];
    203     summary
    204       .addHeading('Zola check results')
    205       .addTable([
    206         [
    207           {data: 'Link Type', header: true},
    208           {data: 'Total', header: true},
    209           {data: 'Result', header: true}
    210         ],
    211         ['Internal', '', 'Pass ✅'],
    212         ['External', totalExternal, `Pass ✅`]
    213       ])
    214       .write();
    215   } else {
    216     // eslint-disable-next-line @typescript-eslint/no-explicit-any
    217     const totalInternal = stdoutParser.results[0].filter((result: any) =>
    218       result.hasOwnProperty('internal_links')
    219     )[0]['internal_links']['total'];
    220     // eslint-disable-next-line @typescript-eslint/no-explicit-any
    221     const totalExternal = stdoutParser.results[0].filter((result: any) =>
    222       result.hasOwnProperty('external_links_planed_checking')
    223     )[0]['external_links_planed_checking']['total'];
    224     const skippedExternal =
    225       // eslint-disable-next-line @typescript-eslint/no-explicit-any
    226       stdoutParser.results[0].filter((result: any) =>
    227         result.hasOwnProperty('external_links_planed_checking')
    228       )[0]['external_links_planed_checking']['skipped'] || '0';
    229     // eslint-disable-next-line @typescript-eslint/no-explicit-any
    230     const errorCount = stdoutParser.results[0].filter((result: any) =>
    231       result.hasOwnProperty('external_links_checked')
    232     )[0]['external_links_checked']['errors'];
    233     summary
    234       .addHeading('Zola check results')
    235       .addTable([
    236         [
    237           {data: 'Link Type', header: true},
    238           {data: 'Total', header: true},
    239           {data: 'Result', header: true}
    240         ],
    241         ['Internal', totalInternal, 'Pass ✅'],
    242         [
    243           'External',
    244           `${totalExternal} (Skipped ${skippedExternal})`,
    245           `Fail (${errorCount} error(s) found) ❌`
    246         ]
    247       ])
    248       .write();
    249   }
    250 }
    251 
    252 run();