#!/bin/sh set -eu usage() { cat <<-'EOF' Usage: aux/ci/report.sh -o OUTDIR aux/ci/report.sh -h EOF } help() { cat <<-'EOF' Options: -o OUTDIR the directory where to place the generated files -h, --help show this message Gather data from Git Notes, and generate an HTML report on CI runs. Two refs with notes are expected: 1. refs/notes/ci-data: contains metadata abount the CI runs, with timestamps, filenames and exit status; 2. refs/notes/ci-logs: contains the content of the log. When reconstructing the CI run, the $FILENAME present in the refs/notes/ci-data ref names the file, and its content comes from refs/notes/ci-logs. On a CI run that generated the numbers from 1 to 10, for a file named 'my-ci-run-2020-01-01-deadbeef.log' that exited successfully, ran for 15 seconds and was deployed to production, the expected output on the target directory "public" is: $ tree public/ public/ index.html data/ 2020-01-01T01:00:00-deadbeef.log ... logs/ 2020-01-01T01:00:00-deadbeef.log ... $ cat public/data/2020-01-01T01:00:00-deadbeef.log status 0 sha deadbeef filename deadbeef 2020-01-01T01:00:00-deadbeef.log duration 15 timestamp 2020-01-01T01:00:00 to-prod true refname refs/heads/main $ cat public/logs/2020-01-01T01:00:00-deadbeef.log 1 2 3 4 5 6 7 8 9 10 The generated 'index.html' is a webpage with the list of all known CI runs, their status, a link to the commit and a link to the log and data files. To enable fetching these refs by default, do so in the git config: $ git config --add remote.origin.fetch '+refs/notes/*:refs/notes/*' Examples: Generate the report on the 'www' directory: $ sh aux/ci/report.sh -o www EOF } for flag in "$@"; do case "$flag" in --) break ;; --help) usage help exit ;; *) ;; esac done while getopts 'o:h' flag; do case "$flag" in o) OUTDIR="$OPTARG" ;; h) usage help exit ;; *) exit 2 ;; esac done shift $((OPTIND - 1)) . aux/lib.sh eval "$(assert_arg "${OUTDIR:-}" '-o OUTDIR')" esc() { sed \ -e 's|&|\&|g' \ -e 's|<|\<|g' \ -e 's|>|\>|g' \ -e 's|"|\"|g' \ -e "s|'|\'|g" } mkdir -p "$OUTDIR" cd "$OUTDIR" mkdir -p logs data for c in $(git notes list | cut -d' ' -f2); do git notes --ref=refs/notes/ci-data show "$c" > data/FILENAME-tmp FILENAME="$(grep '^filename ' data/FILENAME-tmp | cut -d' ' -f2-)" mv data/FILENAME-tmp data/"$FILENAME" git notes --ref=refs/notes/ci-logs show "$c" > logs/"$FILENAME" done { cat <<-EOF <!DOCTYPE html> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="description" content="CI logs for $NAME" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <title>$NAME - CI logs</title> <style> body { max-width: 800px; margin: 0 auto; } code { display: block; margin: 1em 0em 3em 3em; overflow: auto; } pre { display: inline; } ol { list-style-type: disc; } pre, code { background-color: #ddd; } @media(prefers-color-scheme: dark) { :root { color: white; background-color: black; } a { color: hsl(211, 100%, 60%); } a:visited { color: hsl(242, 100%, 80%); } pre, code { background-color: #222; } } </style> </head> <body> <main> <h1> CI logs for <a href="https://$DOMAIN/s/$NAME/">$NAME</a> </h1> <ol> EOF PASS='✅' # ✅ WARN='🐌' # 🐌 FAIL='❌' # ❌ find data/ -type f | LANG=C.UTF-8 sort -r | while read -r f; do STATUS="$( grep '^status ' "$f" | cut -d' ' -f2- | esc)" SHA="$( grep '^sha ' "$f" | cut -d' ' -f2- | esc)" FILENAME="$(grep '^filename ' "$f" | cut -d' ' -f2- | esc)" DURATION="$(grep '^duration ' "$f" | cut -d' ' -f2- | cut -d'"' -f1 | esc)" MESSAGE="$({ git log -1 --format=%B "$SHA" || { git fetch origin "$SHA" git log -1 --format=%B "$SHA" } } | esc)" if [ "$STATUS" = 0 ]; then if [ "$DURATION" -le 60 ]; then STATUS_MARKER="$PASS" else STATUS_MARKER="$WARN" fi else STATUS_MARKER="$FAIL" fi cat <<-EOF <li id="$FILENAME"> <a href="#$FILENAME"><pre>#</pre></a> $STATUS_MARKER - <pre>${DURATION:-?}s</pre> <pre>(<a href="https://$DOMAIN/git/$NAME/commit/?id=$SHA">commit</a>)</pre> <a href="logs/$FILENAME"><pre>$FILENAME</pre></a> <pre>(<a href="data/$FILENAME">data</a>)</pre> <br /> <code><pre>$MESSAGE</pre></code> </li> EOF done cat <<-EOF </ol> </main> </body> </html> EOF } > index.html