aboutsummaryrefslogtreecommitdiff
#!/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|&|\&amp;|g'  \
		-e 's|<|\&lt;|g'   \
		-e 's|>|\&gt;|g'   \
		-e 's|"|\&quot;|g' \
		-e "s|'|\&#39;|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='&#x2705;'  # ✅
	WARN='&#x1F40C;' # 🐌
	FAIL='&#x274C;'  # ❌
	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