summaryrefslogtreecommitdiff
path: root/src/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'src/scripts')
-rwxr-xr-xsrc/scripts/backup.sh149
-rwxr-xr-xsrc/scripts/check.sh92
-rwxr-xr-xsrc/scripts/cicd.sh168
-rwxr-xr-xsrc/scripts/cronjob.sh169
-rwxr-xr-xsrc/scripts/deploy.sh72
-rwxr-xr-xsrc/scripts/gc.sh146
-rwxr-xr-xsrc/scripts/reconfigure.sh146
-rwxr-xr-xsrc/scripts/report.sh260
8 files changed, 1202 insertions, 0 deletions
diff --git a/src/scripts/backup.sh b/src/scripts/backup.sh
new file mode 100755
index 0000000..6a2a4ff
--- /dev/null
+++ b/src/scripts/backup.sh
@@ -0,0 +1,149 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ backup [-q] [-C COMMENT] [-x] [ARCHIVE_TAG]
+ backup -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+
+ Options:
+ -q disable verbose mode, useful for batch sessions
+ -C COMMENT the comment text to be attached to the archive
+ -x disable checking the repository after creating the backup
+ -h, --help show this message
+
+ ARCHIVE_TAG the tag used to create the new
+ backup (default: "manual")
+
+
+ The repository is expected to have been create with:
+
+ $ borg init -e repokey-blake2
+
+ The following environment variables are expected to be exported:
+
+ $BORG_PASSCOMMAND
+ $BORG_REPO
+ $BORG_REMOTE_PATH
+
+ Password-less SSH access is required, usually done via adding
+ /root/.ssh/id_rsa.pub to the ssh remote's
+ $THE_REMOTE:.ssh/authorized_keys
+
+ Root permission is also required.
+
+
+ Examples:
+
+ Run backup from cronjob:
+
+ $ backup
+
+
+ Run backup from cronjob:
+
+ $ backup -q cronjob
+
+
+ Create backup with a comment, a tag, and verbose mode active, and do not
+ verify the repository afterwards:
+
+ $ backup -xC 'The backup has a comment'
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+VERBOSE_FLAGS='--verbose --progress'
+COMMENT=' '
+CHECK=true
+while getopts 'qC:xh' flag; do
+ case "$flag" in
+ q)
+ VERBOSE_FLAGS=''
+ ;;
+ C)
+ COMMENT="$OPTARG"
+ ;;
+ x)
+ CHECK=false
+ ;;
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+ARCHIVE_TAG="${1:-manual}"
+
+
+if [ "$(id -un)" != 'root' ]; then
+ printf 'This script must be run as root.\n\n' >&2
+ usage >&2
+ exit 2
+fi
+
+
+run() {
+ STATUS=0
+ set -x
+ # shellcheck disable=2086
+ sudo -i borg create \
+ $VERBOSE_FLAGS \
+ --comment "$COMMENT" \
+ --stats \
+ --compression lzma,9 \
+ "$BORG_REPO::$(hostname)-{now}-$ARCHIVE_TAG" \
+ /mnt/production/ \
+ /root/ \
+ /home/ \
+ /etc/ \
+ /var/ \
+ /opt/ \
+ /srv/ || STATUS=$?
+ set +x
+
+ if [ "$STATUS" = 0 ]; then
+ return 0
+ elif [ "$STATUS" = 1 ]; then
+ printf 'WARNING, but no ERROR.\n' >&2
+ return 0
+ else
+ return "$STATUS"
+ fi
+}
+
+run
+
+if [ "$CHECK" = true ]; then
+ # shellcheck disable=2086
+ sudo -i borg check $VERBOSE_FLAGS --verify-data "$BORG_REPO"
+fi
diff --git a/src/scripts/check.sh b/src/scripts/check.sh
new file mode 100755
index 0000000..5c63816
--- /dev/null
+++ b/src/scripts/check.sh
@@ -0,0 +1,92 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ check
+ check -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+
+ Options:
+ -h, --help show this message
+
+
+ Run system sanity checks, such as email reachability, alarms
+ reachability, filesystem checks, etc.
+
+
+ Examples:
+
+ Just run it
+
+ $ check
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+
+if [ "$(id -un)" != 'root' ]; then
+ printf 'This script must be run as root.\n\n' >&2
+ usage >&2
+ exit 2
+fi
+
+
+uuid() {
+ od -xN20 /dev/urandom |
+ head -n1 |
+ awk '{OFS="-"; print $2$3,$4,$5,$6,$7$8$9}'
+}
+
+for alias in abuse admin postmaster hostmaster; do
+ uuid | mail -s "\"$alias\" alias test from $(id -un)@$(hostname)" "$alias@$(hostname)"
+done
+
+
+PARTITIONS='
+/dev/vda3
+/dev/vdb1
+/dev/vdc1
+'
+set -x
+
+for part in $PARTITIONS; do
+ btrfs scrub start -B "$part"
+ btrfs check --force -p "$part"
+done
diff --git a/src/scripts/cicd.sh b/src/scripts/cicd.sh
new file mode 100755
index 0000000..58f4fce
--- /dev/null
+++ b/src/scripts/cicd.sh
@@ -0,0 +1,168 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ cicd [-n] NAME [SHA]
+ cicd -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+
+ Options:
+ -n build the system, but don't switch to it (dry-run)
+ -h, --help show this message
+
+ NAME the name of the project
+ SHA the repository SHA to checkout (default: main)
+
+
+ Do a CI/CD run of the project called NAME, located at
+ /srv/git/$NAME.git. If -n is given, only build, otherwise
+ also do the deploy when the build is successfull.
+
+ A "build" consists of:
+ - doing a fresh clone of the project on a temporary directory;
+ - checkout the project to version $SHA;
+ - when a "manifest.scm" file exists in the root of the project,
+ use it to launch a containerized Guix shell; otherwise use a
+ fallback template for a containerized Guix shell;
+ - build the "dev" target of the Makefile via "make dev".
+
+ A "deploy" consists of:
+ - copying the "description" metadata file to
+ /srv/git/$NAME.git/description, in order to update the project
+ repository's description;
+ - upgrading the "pre-receive" Git hook, so that future runs are
+ affected by it;
+ - copying the "public/" directory that the "dev" target built to
+ the /srv/www/s/$NAME/ directory, so that the projects "public/"
+ directory is accessible via the web address
+ "https://euandre.org/s/$NAME/".
+
+ This command must be ran as root.
+
+
+ Examples:
+
+ Build and deploy the "remembering" project on the default branch:
+
+ $ sudo cicd remembering
+
+
+ Build the "urubu" project on a specific commit, but don't deploy:
+
+ $ sudo cicd -n urubu 916dafc092f797349a54515756f2c8e477326511
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+DRY_RUN=false
+while getopts 'nh' flag; do
+ case "$flag" in
+ n)
+ DRY_RUN=true
+ ;;
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+NAME="${1:-}"
+SHA="${2:-main}"
+REPO="/srv/git/$NAME.git"
+
+if [ -z "$NAME" ]; then
+ printf 'Missing NAME.\n\n' >&2
+ usage >&2
+ exit 2
+fi
+
+if [ "$(id -un)" != 'root' ]; then
+ printf 'This script must be run as root.\n\n' >&2
+ usage >&2
+ exit 2
+fi
+
+
+set +eu
+# shellcheck source=/dev/null
+. /etc/rc
+set -eu
+
+
+uuid() {
+ od -xN20 /dev/urandom |
+ head -n1 |
+ awk '{OFS="-"; print $2$3,$4,$5,$6,$7$8$9}'
+}
+
+tmpname() {
+ printf '%s/uuid-tmpname with spaces.%s' "${TMPDIR:-/tmp}" "$(uuid)"
+}
+
+mkdtemp() {
+ name="$(tmpname)"
+ mkdir -- "$name"
+ printf '%s' "$name"
+}
+
+
+TMP="$(mkdtemp)"
+trap 'rm -rf "$TMP"' EXIT
+
+
+set -x
+chown deployer:deployer "$TMP"
+cd "$TMP"
+sudo -u deployer git clone "$REPO" .
+sudo -u deployer --preserve-env=GIT_CONFIG_GLOBAL git checkout "$SHA"
+guix system describe
+
+if [ -f manifest.scm ]; then
+ guix shell -Cv3 -m manifest.scm -- make dev
+else
+ guix shell -Cv3 -- make dev
+fi
+
+if [ "$DRY_RUN" = false ]; then
+ # COMMENT: pre-receive is always running the previous version!
+ # The same is true for the reconfigure script itself.
+ sudo cp description "$REPO"/description
+ sudo cp aux/ci/git-pre-receive.sh "$REPO"/hooks/pre-receive
+
+ sudo -u deployer rsync \
+ --delete \
+ --chmod=D775,F664 \
+ --chown=deployer:deployer \
+ --exclude 'ci/*' \
+ -a \
+ public/ /srv/www/s/"$NAME"/
+fi
diff --git a/src/scripts/cronjob.sh b/src/scripts/cronjob.sh
new file mode 100755
index 0000000..4cd456e
--- /dev/null
+++ b/src/scripts/cronjob.sh
@@ -0,0 +1,169 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ cronjob COMMAND...
+ cronjob -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+
+ Options:
+ -h, --help show this message
+
+ COMMAND the command to be executed
+
+
+ Execute the given command, and send the output to email, with
+ special treatment to the status code. It kills the job it it
+ lasts more than one hour.
+
+ It load the appropriate files, so that the actual cron
+ declaration is smaller.
+
+
+ Examples:
+
+ Run a backup:
+
+ $ cronjob backup -q cron
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+if [ -z "${1:-}" ]; then
+ printf 'Missing COMMAND.\n\n' >&2
+ usage >&2
+ exit 2
+fi
+
+if [ "$(id -un)" != 'root' ]; then
+ printf 'This script must be run as root.\n\n' >&2
+ usage >&2
+ exit 2
+fi
+
+
+set +eu
+# shellcheck source=/dev/null
+. /etc/rc
+set -eu
+
+
+
+epoch() {
+ awk 'BEGIN { srand(); print(srand()); }'
+}
+
+now() {
+ date '+%Y-%m-%dT%H:%M:%S%:z'
+}
+
+
+uuid() {
+ od -xN20 /dev/random |
+ head -n1 |
+ awk '{OFS="-"; print $2$3,$4,$5,$6,$7$8$9}'
+}
+
+mkstemp() {
+ name="${TMPDIR:-/tmp}/uuid-tmpname with spaces.$(uuid)"
+ touch "$name"
+ printf '%s' "$name"
+}
+
+pre() {
+ # Same as:
+ # sed -u "s|^|[$CMD]: |"
+ # but the "-u" option is not POSIX
+ IFS=''
+ while read -r line; do
+ printf '[%s]: %s\n' "$CMD" "$line"
+ done
+}
+
+duration() {
+ minutes=$((${1} / 60))
+ seconds=$((${1} % 60))
+ printf '%sm%ss' "$minutes" "$seconds"
+}
+
+
+CMD="$*"
+HOSTNAME="$(hostname)"
+FROM="cronjob@$HOSTNAME"
+TIMEOUT='10800' # three hours
+STATUS_F="$(mkstemp)"
+OUT="$(mkstemp)"
+
+email() {
+ {
+ cat <<-EOF
+ Content-Type: text/plain; charset=UTF-8
+ Content-Transfer-Encoding: 8bit
+ From: $FROM
+ To: root@localhost
+ Subject: (exit status: $(cat "$STATUS_F")) - $HOSTNAME: $CMD
+
+ EOF
+ cat "$OUT"
+ } | sendmail -t -f "$FROM"
+ rm -f "$OUT" "$STATUS_F"
+}
+trap email EXIT
+
+{
+ cat <<-EOF
+ Running commad: $CMD
+ Starting at: $(now)
+
+ EOF
+
+ START="$(epoch)"
+ STATUS=0
+ timeout "$TIMEOUT" "$@" || STATUS=$?
+ printf '%s' "$STATUS" > "$STATUS_F"
+ END="$(epoch)"
+ DURATION_SECONDS=$((END - START))
+
+ cat <<-EOF
+
+ Finished at: $(now)
+ Duration: $(duration "$DURATION_SECONDS")
+ EOF
+} 2>&1 | pre | ts '%Y-%m-%dT%H:%M:%S' | tee "$OUT" >> /var/log/cronjobs.log
diff --git a/src/scripts/deploy.sh b/src/scripts/deploy.sh
new file mode 100755
index 0000000..dc30484
--- /dev/null
+++ b/src/scripts/deploy.sh
@@ -0,0 +1,72 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ deploy
+ deploy -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+
+ Options:
+ -h, --help show this message
+
+
+ Do a blue/green deployment of the main service. It makes sure
+ that the new service is up and running before shutting down the
+ old one.
+
+
+ Examples:
+
+ Just do the deploy:
+
+ $ deploy
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+
+if [ "$(id -un)" != 'root' ]; then
+ printf 'This script must be run as root.\n\n' >&2
+ usage >&2
+ exit 2
+fi
+
+
+echo FIXME
diff --git a/src/scripts/gc.sh b/src/scripts/gc.sh
new file mode 100755
index 0000000..e037f3c
--- /dev/null
+++ b/src/scripts/gc.sh
@@ -0,0 +1,146 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ gc [TYPE]
+ gc -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+
+ Options:
+ -h, --help show this message
+
+ TYPE what to do GC on (default: all):
+ - guix
+ - deploy
+ - trash
+ - tmpdir
+ - logs
+
+
+ GC the server, deleting old, unusable data, in order to free
+ disk space system-wide.
+
+
+ Examples:
+
+ Just run it, for all:
+
+ $ gc
+
+
+ Cleanup tmpdir:
+
+ $ gc tmpdir
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+
+if [ "$(id -un)" != 'root' ]; then
+ printf 'This script must be run as root.\n\n' >&2
+ usage >&2
+ exit 2
+fi
+
+
+disk() {
+ df -h / /mnt/backup/ |
+ tail -n +2 |
+ awk '{ printf "%s\t%s/%s\t%s\n", $4, $3, $2, $6 }'
+}
+
+today() {
+ date '+%Y-%m-%d'
+}
+
+gc_guix() {
+ sudo -i guix system delete-generations 1m
+ sudo -i guix gc -d 1m
+}
+
+gc_deploy() {
+ find /opt/deploy \
+ ! -path /opt/deploy -prune \
+ -type d \
+ -not -name "$(today)*" \
+ -exec rm -rvf "{}" ';'
+}
+
+gc_trash() {
+ yes | sudo -i trash-empty
+}
+
+gc_tmpdir() {
+ find "${TMPDIR:-/tmp}" -atime +10 -exec rm -vf "{}" ';'
+}
+
+gc_logs() {
+ find /var/log/ci/ -atime +10 -exec rm -vf "{}" ';'
+}
+
+
+gc_all() {
+ gc_guix
+ gc_deploy
+ gc_trash
+ gc_tmpdir
+ gc_logs
+}
+
+
+TYPE="${1:-all}"
+CMD=gc_"$TYPE"
+if ! command -v "$CMD" >/dev/null; then
+ printf 'Invalid TYPE: "%s".\n\n' "$TYPE" >&2
+ usage >&2
+ exit 2
+fi
+
+BEFORE="$(disk)"
+set -x
+"$CMD"
+set +x
+AFTER="$(disk)"
+
+cat <<-EOF
+ Disk space:
+ before: $BEFORE
+ after: $AFTER
+EOF
diff --git a/src/scripts/reconfigure.sh b/src/scripts/reconfigure.sh
new file mode 100755
index 0000000..8fa47c5
--- /dev/null
+++ b/src/scripts/reconfigure.sh
@@ -0,0 +1,146 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ reconfigure [-n] [-U] [SHA]
+ reconfigure -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+
+ Options:
+ -n build the system, but don't switch to it (dry-run)
+ -U pull the latest channels before reconfiguring
+ -h, --help show this message
+
+ SHA the repository SHA to checkout (default: main)
+
+
+ Run a "guix system reconfigure" as root via "sudo -i". If a -U
+ flag is given, perform a "guix pull" (in root profile) prior to
+ the reconfigure. The user must be able to become the "deployer"
+ user, either via "sudo reconfigure" or by being member of the
+ "become-deployer" group.
+
+
+ Examples:
+
+ Reconfigure the system:
+
+ $ reconfigure
+
+
+ Build the system on a custom SHA, but don't switch to it:
+
+ $ reconfigure -n 916dafc092f797349a54515756f2c8e477326511
+
+
+ Update and upgrade:
+
+ $ reconfigure -U
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+UPDATE=false
+DRY_RUN=false
+while getopts 'nUh' flag; do
+ case "$flag" in
+ n)
+ DRY_RUN=true
+ ;;
+ U)
+ UPDATE=true
+ ;;
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+# shellcheck source=/dev/null
+. /etc/conf.env
+SHA="${1:-main}"
+REPO="/srv/git/$REPO_NAME"
+NOW="$(date '+%Y-%m-%dT%H:%M:%S%:z')"
+NOW_DIR=/opt/deploy/"$NOW"
+NPROC=$(($(nproc) * 2 + 1))
+
+
+if [ "$(id -un)" != 'root' ]; then
+ printf 'This script must be run as root.\n\n' >&2
+ usage >&2
+ exit 2
+fi
+
+
+set +eu
+# shellcheck source=/dev/null
+. /etc/rc
+set -eu
+
+
+if [ "$UPDATE" = true ] && [ "$DRY_RUN" = false ]; then
+ sudo -i guix pull -v3
+fi
+
+set -x
+sudo -u deployer git clone --depth=1 "file://$REPO" "$NOW_DIR"
+sudo -u deployer rm -f /opt/deploy/current
+sudo -u deployer ln -rs "$NOW_DIR" /opt/deploy/current
+cd /opt/deploy/current
+sudo -u deployer git fetch --depth=1 "file://$REPO" "$SHA"
+sudo -u deployer --preserve-env=GIT_CONFIG_GLOBAL git checkout "$SHA"
+guix system describe
+
+if [ "$DRY_RUN" = true ]; then
+ sudo -i guix system -c$NPROC -v3 build "$PWD"/src/infrastructure/guix/system.scm
+else
+ # COMMENT: pre-receive is always running the previous version!
+ # The same is true for the reconfigure script itself.
+ cp description "$REPO"/description
+ cp src/infrastructure/ci/git-pre-receive.sh "$REPO"/hooks/pre-receive
+ cp src/infrastructure/guix/channels.scm /etc/guix/
+ cp src/infrastructure/guix/system.scm /etc/guix/
+
+ sudo -i guix system -c$NPROC -v3 reconfigure /etc/guix/system.scm
+
+ sudo -u deployer rsync \
+ --delete \
+ --chmod=D775,F664 \
+ --chown=deployer:deployer \
+ --exclude "$CI_SUFFIX/*" \
+ -a \
+ /run/current-system/profile/share/doc/"$NAME"/ "$HTML_OUTDIR_TOP"/
+
+ ln -sf /var/log/"$NAME".log "$HTML_OUTDIR_PRIV"
+
+ deploy
+fi
diff --git a/src/scripts/report.sh b/src/scripts/report.sh
new file mode 100755
index 0000000..e14e40a
--- /dev/null
+++ b/src/scripts/report.sh
@@ -0,0 +1,260 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ report [-C REPO] -o DIRECTORY
+ report -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+
+ Options:
+ -C REPO change to REPO when doing Git operations (default: $PWD)
+ -o DIRECTORY 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, the
+ expected output on the target directory "public" is:
+
+ $ tree public/
+ public/
+ index.html
+ data/
+ my-ci-run-2020-01-01-deadbeef.log
+ ...
+ logs/
+ my-ci-run-2020-01-01-deadbeef.log
+ ...
+
+ $ cat public/data/my-ci-run-2020-01-01-deadbeef.log
+ 0 deadbeef my-ci-run-2020-01-01-deadbeef.log
+
+ $ cat public/logs/my-ci-run-2020-01-01-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 file.
+
+ 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:
+
+ $ report -o www
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+REPO="$PWD"
+while getopts 'C:o:h' flag; do
+ case "$flag" in
+ C)
+ REPO="$OPTARG"
+ ;;
+ o)
+ OUTDIR="$OPTARG"
+ ;;
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+if [ -z "${OUTDIR:-}" ]; then
+ printf 'Missing -o OUTDIR.\n\n' >&2
+ usage >&2
+ exit 2
+fi
+
+if [ -r src/infrastructure/config/conf.env ]; then
+ CONF=src/infrastructure/config/conf.env
+else
+ CONF=/etc/conf.env
+fi
+
+# shellcheck source=/dev/null
+. "$CONF"
+
+
+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 -C "$REPO" notes list | cut -d' ' -f2); do
+ git -C "$REPO" 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 -C "$REPO" notes --ref=refs/notes/ci-logs show "$c" > logs/"$FILENAME"
+done
+
+{
+ cat <<-EOF
+ <!DOCTYPE html>
+ <html lang="en">
+ <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="$HOMEPAGE">$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 -C "$REPO" log -1 --format=%B "$SHA" || {
+ git fetch origin "$SHA"
+ git -C "$REPO" 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="${CGIT_URL}${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