aboutsummaryrefslogtreecommitdiff
path: root/aux
diff options
context:
space:
mode:
authorEuAndreh <eu@euandre.org>2022-01-18 11:40:47 -0300
committerEuAndreh <eu@euandre.org>2022-01-18 14:02:59 -0300
commit47bfd2ed8c3219e79f8974a8fc2ac9265ed91bd2 (patch)
tree3d162596d874b4f775da12c843ad3918b593f713 /aux
parentInitial empty commit (diff)
downloadtd-47bfd2ed8c3219e79f8974a8fc2ac9265ed91bd2.tar.gz
td-47bfd2ed8c3219e79f8974a8fc2ac9265ed91bd2.tar.xz
First commit, now with a clean history
Diffstat (limited to 'aux')
-rwxr-xr-xaux/80-columns.sh12
-rwxr-xr-xaux/assert-shellcheck.sh6
-rwxr-xr-xaux/ci/ci-build.sh60
-rwxr-xr-xaux/ci/git-post-receive.sh22
-rwxr-xr-xaux/ci/git-pre-push.sh22
-rwxr-xr-xaux/ci/report.sh109
-rw-r--r--aux/containers/guix/manifest.scm23
-rwxr-xr-xaux/containers/guix/with-container.sh19
-rwxr-xr-xaux/lib.sh30
-rw-r--r--aux/tld.txt1
l---------aux/with-container1
-rwxr-xr-xaux/workflow/TODOs.sh64
-rwxr-xr-xaux/workflow/assert-changelog.sh66
-rwxr-xr-xaux/workflow/assert-readme.sh108
-rwxr-xr-xaux/workflow/assert-todos.sh58
-rwxr-xr-xaux/workflow/commonmark.sh48
-rwxr-xr-xaux/workflow/dist.sh112
-rw-r--r--aux/workflow/favicon.html1
-rw-r--r--aux/workflow/favicon.svg62
-rwxr-xr-xaux/workflow/l10n.sh109
-rw-r--r--aux/workflow/preamble.md16
-rwxr-xr-xaux/workflow/public.sh83
-rwxr-xr-xaux/workflow/sign-tarballs.sh38
-rw-r--r--aux/workflow/style.css62
24 files changed, 1132 insertions, 0 deletions
diff --git a/aux/80-columns.sh b/aux/80-columns.sh
new file mode 100755
index 0000000..ae4660e
--- /dev/null
+++ b/aux/80-columns.sh
@@ -0,0 +1,12 @@
+#!/bin/sh
+set -eu
+
+# shellcheck disable=2068
+for f in ${@:-$(cat -)}; do
+ if [ "$(file -i "$f" | cut -d' ' -f2 | cut -d/ -f1)" = 'text' ]; then
+ sed 's/\t/ /g' "$f" |
+ awk -v FNAME="$f" 'length > 80 {
+ printf "%s:%s:%s\n", FNAME, NR, $0
+ }'
+ fi
+done
diff --git a/aux/assert-shellcheck.sh b/aux/assert-shellcheck.sh
new file mode 100755
index 0000000..40fd364
--- /dev/null
+++ b/aux/assert-shellcheck.sh
@@ -0,0 +1,6 @@
+#!/bin/sh
+set -eu
+
+find . -name '*.sh' -print0 |
+ xargs -0 awk 'FNR==1 && /^#!\/bin\/sh$/ { print FILENAME }' |
+ xargs shellcheck
diff --git a/aux/ci/ci-build.sh b/aux/ci/ci-build.sh
new file mode 100755
index 0000000..34233b7
--- /dev/null
+++ b/aux/ci/ci-build.sh
@@ -0,0 +1,60 @@
+#!/bin/sh
+set -eux
+
+PROJECT="$1"
+LOGS_DIR="$2"
+SHA="$3"
+FILENAME="$(date -Is)-$SHA.log"
+LOGFILE="$LOGS_DIR/$FILENAME"
+
+mkdtemp() {
+ name="$(echo 'mkstemp(template)' |
+ m4 -D template="${TMPDIR:-/tmp}/m4-tmpname.")"
+ rm -f "$name"
+ mkdir "$name"
+ echo "$name"
+}
+
+{
+ echo "Starting CI job at: $(date -Is)"
+
+ finish() {
+ STATUS="$?"
+ printf "\n\n>>> exit status was %s\n" "$STATUS"
+ echo "Finishing CI job at: $(date -Is)"
+ cd -
+ NOTE=$(cat <<EOF
+See CI logs with:
+ git notes --ref=refs/notes/ci-logs show $SHA
+ git notes --ref=refs/notes/ci-data show $SHA
+EOF
+)
+ git notes --ref=refs/notes/ci-data add -f -m "$STATUS $FILENAME"
+ git notes --ref=refs/notes/ci-logs add -f -F "$LOGFILE"
+ git notes append -m "$NOTE"
+
+ cd -
+ git fetch origin refs/notes/*:refs/notes/*
+ sh aux/ci/report.sh -n "$PROJECT" -o public
+ rsync -av public/ "/srv/http/$PROJECT/" --delete
+
+ printf '\n>>>\n>>> CI logs added as Git note.\n>>>\n>>> Run status was %s\n>>>\n\n' \
+ "$STATUS"
+ }
+ trap finish EXIT
+
+ unset GIT_DIR
+ REMOTE="$PWD"
+ cd "$(mkdtemp)"
+ git clone "$REMOTE" .
+ git config --global user.email git@euandre.org
+ git config --global user.name 'EuAndreh CI'
+
+ if [ -e aux/with-container ]; then
+ RUNNER='sh aux/with-container'
+ else
+ RUNNER='sh -c'
+ fi
+
+ $RUNNER 'make clean public dev-check'
+} 2>&1 | tee "$LOGFILE"
diff --git a/aux/ci/git-post-receive.sh b/aux/ci/git-post-receive.sh
new file mode 100755
index 0000000..92bba73
--- /dev/null
+++ b/aux/ci/git-post-receive.sh
@@ -0,0 +1,22 @@
+#!/bin/sh
+set -eu
+
+for n in $(seq 0 $((GIT_PUSH_OPTION_COUNT - 1))); do
+ opt="$(eval "echo \$GIT_PUSH_OPTION_$n")"
+ if [ "$opt" = skip-ci ] || [ "$opt" = ci-skip ]; then
+ printf "\n'%s' option detected, not running ci-build.sh\n\n" \
+ "$opt"
+ exit 0
+ fi
+done
+
+# shellcheck disable=2034
+read -r _oldrev SHA _refname
+
+PROJECT="$(basename "$PWD" | cut -d. -f1)" # remove .git suffix
+LOGS_DIR="/opt/ci/$PROJECT/logs"
+sh "/opt/ci/$PROJECT/ci-build.sh" "$PROJECT" "$LOGS_DIR" "$SHA" ||:
+
+echo 'To retrigger the build, run:'
+echo "cd /srv/http/$PROJECT.git/"
+echo "sh /opt/ci/$PROJECT/ci-build.sh" "$PROJECT" "$LOGS_DIR" "$SHA"
diff --git a/aux/ci/git-pre-push.sh b/aux/ci/git-pre-push.sh
new file mode 100755
index 0000000..eaaa7bd
--- /dev/null
+++ b/aux/ci/git-pre-push.sh
@@ -0,0 +1,22 @@
+#!/bin/sh
+set -eux
+
+TLD="$(cat aux/tld.txt)"
+. aux/lib.sh
+
+PROJECT="$(basename "$PWD")"
+LOGS_DIR="/opt/ci/$PROJECT/logs"
+REMOTE_GIT_DIR="/srv/http/$PROJECT.git"
+
+DESCRIPTION="$(mkstemp)"
+if [ -f description ]
+then
+ cp description "$DESCRIPTION"
+else
+ git config euandreh.description > "$DESCRIPTION"
+fi
+
+scp "$DESCRIPTION" "$TLD:$REMOTE_GIT_DIR/description"
+ssh "$TLD" mkdir -p "$LOGS_DIR"
+scp aux/ci/ci-build.sh "$TLD:$(dirname "$LOGS_DIR")/ci-build.sh"
+scp aux/ci/git-post-receive.sh "$TLD:$REMOTE_GIT_DIR/hooks/post-receive"
diff --git a/aux/ci/report.sh b/aux/ci/report.sh
new file mode 100755
index 0000000..e900e26
--- /dev/null
+++ b/aux/ci/report.sh
@@ -0,0 +1,109 @@
+#!/bin/sh
+set -eu
+
+TLD="$(cat aux/tld.txt)"
+. aux/lib.sh
+
+while getopts 'n:o:' flag; do
+ case "$flag" in
+ n)
+ PROJECT="$OPTARG"
+ ;;
+ o)
+ OUTDIR="$OPTARG"
+ ;;
+ *)
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+assert_arg() {
+ if [ -z "$1" ]; then
+ echo "Missing $2" >&2
+ exit 2
+ fi
+}
+
+assert_arg "${PROJECT:-}" '-n PROJECT'
+assert_arg "${OUTDIR:-}" '-o OUTDIR'
+
+PASS='✅'
+FAIL='❌'
+
+mkdir -p "$OUTDIR/ci-logs" "$OUTDIR/ci-data"
+
+OUT="$(mkstemp)"
+chmod 644 "$OUT"
+
+for c in $(git notes list | cut -d\ -f2); do
+ DATA="$(git notes --ref=refs/notes/ci-data show "$c")"
+ FILENAME="$(echo "$DATA" | cut -d\ -f2)"
+ echo "$DATA" > "$OUTDIR/ci-data/$FILENAME"
+ git notes --ref=refs/notes/ci-logs show "$c" \
+ > "$OUTDIR/ci-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 $PROJECT" />
+ <link rel="icon" type="image/svg+xml" href="favicon.svg" />
+ <title>$PROJECT - CI logs</title>
+
+EOF
+
+ cat aux/workflow/style.css
+
+ cat <<EOF
+
+ <style>
+ pre {
+ display: inline;
+ }
+ ol {
+ list-style-type: disc;
+ }
+ </style>
+ </head>
+ <body>
+ <main>
+ <h1>
+ CI logs for
+ <a href="https://$TLD/$PROJECT/en/">$PROJECT</a>
+ </h1>
+ <ol>
+EOF
+} > "$OUT"
+
+for f in $(find "$OUTDIR/ci-data/" -type f | LANG=C.UTF-8 sort -r); do
+ DATA="$(cat "$f")"
+ STATUS="$(echo "$DATA" | cut -d\ -f1)"
+ FILENAME="$(echo "$DATA" | cut -d\ -f2)"
+
+ if [ "$STATUS" = 0 ]; then
+ STATUS_MARKER="$PASS"
+ else
+ STATUS_MARKER="$FAIL"
+ fi
+
+ cat <<EOF >> "$OUT"
+ <li>
+ <a href="ci-logs/$FILENAME">$STATUS_MARKER <pre>$FILENAME</pre></a>
+ </li>
+EOF
+done
+
+cat <<EOF >> "$OUT"
+ </ol>
+ </main>
+ </body>
+</html>
+EOF
+
+mv "$OUT" "$OUTDIR/ci.html"
diff --git a/aux/containers/guix/manifest.scm b/aux/containers/guix/manifest.scm
new file mode 100644
index 0000000..7d07345
--- /dev/null
+++ b/aux/containers/guix/manifest.scm
@@ -0,0 +1,23 @@
+(specifications->manifest
+ (map symbol->string
+ '(bash
+ coreutils
+ findutils
+ diffutils
+ grep
+ sed
+ m4
+ git
+ gawk
+ make
+ graphviz
+ shellcheck
+ pandoc
+ gettext
+ po4a-text
+ mdpo-patched
+ hunspell
+ hunspell-dict-en-utf8
+ hunspell-dict-pt-utf8
+ hunspell-dict-fr-utf8
+ hunspell-dict-eo-utf8)))
diff --git a/aux/containers/guix/with-container.sh b/aux/containers/guix/with-container.sh
new file mode 100755
index 0000000..fcbb8b5
--- /dev/null
+++ b/aux/containers/guix/with-container.sh
@@ -0,0 +1,19 @@
+#!/bin/sh
+set -eu
+
+CHANNEL_REMOTE='https://euandreh.xyz/package-repository.git'
+CHANNEL_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/euandreh-guix-channel"
+
+if [ ! -d "$CHANNEL_DIR" ]; then
+ git clone "$CHANNEL_REMOTE" "$CHANNEL_DIR"
+fi
+
+git -C "$CHANNEL_DIR" pull
+
+ENV_CMD="guix environment -L $CHANNEL_DIR/src/ -m aux/containers/guix/manifest.scm"
+
+if [ -z "${1:-}" ]; then
+ $ENV_CMD
+else
+ $ENV_CMD --pure -C -- sh -c "$@"
+fi
diff --git a/aux/lib.sh b/aux/lib.sh
new file mode 100755
index 0000000..b47812d
--- /dev/null
+++ b/aux/lib.sh
@@ -0,0 +1,30 @@
+#!/bin/sh
+
+#
+# Generally, utilities that I expected to exist in POSIX, but don't.
+#
+
+uuid() {
+ # Taken from:
+ # https://serverfault.com/a/799198
+ od -xN20 /dev/urandom |
+ head -n1 |
+ awk '{OFS="-"; print $2$3,$4,$5,$6,$7$8$9}'
+}
+
+tmpname() {
+ echo 'mkstemp(template)' | m4 -D template="${TMPDIR:-/tmp}/m4-tmpname."
+}
+
+mkstemp() {
+ name="$(tmpname)"
+ touch "$name"
+ echo "$name"
+}
+
+mkdtemp() {
+ name="$(tmpname)"
+ rm -f "$name"
+ mkdir "$name"
+ echo "$name"
+}
diff --git a/aux/tld.txt b/aux/tld.txt
new file mode 100644
index 0000000..0cb8b8b
--- /dev/null
+++ b/aux/tld.txt
@@ -0,0 +1 @@
+euandreh.xyz
diff --git a/aux/with-container b/aux/with-container
new file mode 120000
index 0000000..b96cf67
--- /dev/null
+++ b/aux/with-container
@@ -0,0 +1 @@
+containers/guix/with-container.sh \ No newline at end of file
diff --git a/aux/workflow/TODOs.sh b/aux/workflow/TODOs.sh
new file mode 100755
index 0000000..b7cbae1
--- /dev/null
+++ b/aux/workflow/TODOs.sh
@@ -0,0 +1,64 @@
+#!/bin/sh
+set -eu
+
+TLD="$(cat aux/tld.txt)"
+PROJECT_UC=
+while getopts 'n:N:m:o:' flag; do
+ case "$flag" in
+ n)
+ PROJECT="$OPTARG"
+ ;;
+ N)
+ PROJECT_UC="$OPTARG"
+ ;;
+ m)
+ MAILING_LIST="$OPTARG"
+ ;;
+ o)
+ OUTDIR="$OPTARG"
+ ;;
+ *)
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+assert_arg() {
+ if [ -z "$1" ]; then
+ echo "Missing $2" >&2
+ exit 2
+ fi
+}
+
+assert_arg "${PROJECT:-}" '-n PROJECT'
+assert_arg "${MAILING_LIST:-}" '-m MAILING_LIST'
+assert_arg "${OUTDIR:-}" '-o OUTDIR'
+
+if [ -z "${PROJECT_UC:-}" ]; then
+ PROJECT_UC="$PROJECT"
+fi
+
+
+cat aux/workflow/preamble.md TODOs.md |
+ td -H |
+ sed \
+ -e "s:@PROJECT_UC@:$PROJECT_UC:g" \
+ -e "s:@PROJECT@:$PROJECT:g" \
+ -e "s:@MAILING_LIST@:$MAILING_LIST:g" \
+ -e "s:@TLD@:$TLD:g" |
+ pandoc \
+ --toc \
+ --highlight-style pygments \
+ --toc-depth=2 \
+ -s \
+ --metadata title="$PROJECT_UC - TODOs" \
+ --metadata lang=en \
+ -r commonmark \
+ -w html \
+ -H aux/workflow/favicon.html \
+ -H aux/workflow/style.css |
+ sed \
+ -e 's:<a><a:<a:g' \
+ -e 's:</a></a>:</a>:g' \
+ > "$OUTDIR/TODOs.html"
diff --git a/aux/workflow/assert-changelog.sh b/aux/workflow/assert-changelog.sh
new file mode 100755
index 0000000..c58a600
--- /dev/null
+++ b/aux/workflow/assert-changelog.sh
@@ -0,0 +1,66 @@
+#!/bin/sh
+set -eu
+
+TLD="$(cat aux/tld.txt)"
+PROJECT_UC=
+while getopts 'n:N:' flag; do
+ case "$flag" in
+ n)
+ PROJECT="$OPTARG"
+ ;;
+ N)
+ PROJECT_UC="$OPTARG"
+ ;;
+ *)
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+assert_arg() {
+ if [ -z "$1" ]; then
+ echo "Missing $2" >&2
+ exit 2
+ fi
+}
+
+assert_arg "${PROJECT:-}" '-n PROJECT'
+
+if [ -z "${PROJECT_UC:-}" ]; then
+ PROJECT_UC="$PROJECT"
+fi
+
+HOMEPAGE_LINK="Changelog for [$PROJECT_UC](https://$TLD/$PROJECT/en/)."
+
+if ! grep -qF "$HOMEPAGE_LINK" CHANGELOG.md; then
+ echo "Missing link to homepage in CHANGELOG.md:" >&2
+ echo "$HOMEPAGE_LINK"
+ exit 1
+fi
+
+assert() {
+ DATE="$1"
+ VVERSION="$2"
+ VERSION="${2#v}"
+ CHANGELOG_ENTRY="$(printf \
+ '# [%s](https://euandreh.xyz/%s.git/commit/?id=%s) - %s' \
+ "$VERSION" "$PROJECT" "$VVERSION" "$DATE")"
+ if ! grep -qF "$CHANGELOG_ENTRY" CHANGELOG.md; then
+ echo "Missing '$CHANGELOG_ENTRY' entry from CHANGELOG.md" >&2
+ exit 1
+ fi
+}
+
+if [ -e .git ]; then
+ for VVERSION in $(git tag); do
+ DATE="$(git log -1 --format=%cd --date=short "$VVERSION")"
+ assert "$DATE" "$VVERSION"
+ done
+fi
+
+# "$@" represents a list of tags to be also included in the verification.
+for VVERSION in "$@"; do
+ DATE="$(date '+%Y-%m-%d')"
+ assert "$DATE" "$VVERSION"
+done
diff --git a/aux/workflow/assert-readme.sh b/aux/workflow/assert-readme.sh
new file mode 100755
index 0000000..0a85221
--- /dev/null
+++ b/aux/workflow/assert-readme.sh
@@ -0,0 +1,108 @@
+#!/bin/sh
+set -eu
+
+if [ ! -e .git ]; then
+ exit
+fi
+
+TLD="$(cat aux/tld.txt)"
+. aux/lib.sh
+
+while getopts 'n:m:' flag; do
+ case "$flag" in
+ n)
+ PROJECT="$OPTARG"
+ ;;
+ m)
+ MAILING_LIST="$OPTARG"
+ ;;
+ *)
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+assert_arg() {
+ if [ -z "$1" ]; then
+ echo "Missing $2" >&2
+ exit 2
+ fi
+}
+
+assert_arg "${PROJECT:-}" '-n PROJECT'
+assert_arg "${MAILING_LIST:-}" '-m MAILING_LIST'
+
+EXPECTED="$(mkstemp)"
+cat <<EOF >> "$EXPECTED"
+
+For running the extra development-only checks, run:
+
+\`\`\`shell
+$ make dev-check
+\`\`\`
+
+and for generating the documentation HTML and website, run:
+
+\`\`\`shell
+$ make public
+\`\`\`
+
+Send contributions to the [mailing list] via
+[\`git send-email\`](https://git-send-email.io/).
+
+
+## Links
+
+- [homepage](https://$TLD/$PROJECT/en/)
+- [source code](https://euandreh.xyz/$PROJECT.git/)
+- [bug tracking](https://$TLD/$PROJECT/TODOs.html)
+- [mailing list]
+- [CI logs](https://$TLD/$PROJECT/ci.html)
+- [CHANGELOG](https://$TLD/$PROJECT/en/CHANGELOG.html)
+
+[mailing list]: https://lists.sr.ht/~euandreh/$MAILING_LIST?search=%5B$PROJECT%5D
+EOF
+
+RELEASES_LIST="$(mkstemp)"
+add_release() {
+ DATE="$1"
+ VVERSION="$2"
+ echo "- [$VVERSION](https://euandreh.xyz/$PROJECT.git/commit/?id=$VVERSION) [$PROJECT-$VVERSION.tar.gz](https://euandreh.xyz/$PROJECT.git/snapshot/$PROJECT-$VVERSION.tar.gz) ([sig](https://euandreh.xyz/$PROJECT.git/snapshot/$PROJECT-$VVERSION.tar.gz.asc)) - $DATE" >> "$RELEASES_LIST"
+}
+
+for VVERSION in $(git tag); do
+ DATE="$(git log -1 --format=%cd --date=short "$VVERSION")"
+ add_release "$DATE" "$VVERSION"
+done
+
+# "$@" represents a list of tags to be also included in the verification.
+for VVERSION in "$@"; do
+ if ! git tag | grep -qF "$VVERSION"; then
+ DATE="$(date '+%Y-%m-%d')"
+ add_release "$DATE" "$VVERSION"
+ fi
+done
+
+if [ -s "$RELEASES_LIST" ]; then
+ printf '\n\n## Releases\n\n' >> "$EXPECTED"
+ sort -r "$RELEASES_LIST" >> "$EXPECTED"
+fi
+
+cat <<EOF >> "$EXPECTED"
+
+
+## License
+
+The code is licensed under
+[GNU Affero General Public License v3.0 or later][AGPL-3.0-or-later]
+(AGPL-3.0-or-later).
+
+[AGPL-3.0-or-later]: https://euandreh.xyz/$PROJECT.git/tree/COPYING
+EOF
+
+if ! tail -n "$(wc -l < "$EXPECTED")" README.md | diff - "$EXPECTED"; then
+ echo 'Wrong metadata at the end of README.md file'
+ echo "See expected content at: $EXPECTED"
+ exit 1
+fi
diff --git a/aux/workflow/assert-todos.sh b/aux/workflow/assert-todos.sh
new file mode 100755
index 0000000..bc4907d
--- /dev/null
+++ b/aux/workflow/assert-todos.sh
@@ -0,0 +1,58 @@
+#!/bin/sh
+set -eu
+
+if [ -e .git ] && git grep FIXME | grep -v '^TODOs.md' |
+ grep -v '^aux/workflow/assert-todos.sh'; then
+ echo "Found dangling FIXME markers on the project."
+ echo "You should write them down properly on TODOs.md."
+ exit 1
+fi
+
+awk -F'{#' '
+BEGIN {
+ exitstatus = 0
+ h2flag = 0
+ h2status = ""
+ prevline = ""
+ idx = 0
+ delete ids[0]
+}
+h2flag == 1 {
+ split($0, l, " ")
+ timelinestatus = l[2]
+ if (h2status != timelinestatus) {
+ print "h2/timeline status mismatch for line " NR-1
+ print prevline
+ print $0
+ exitstatus = 1
+ }
+ h2status = ""
+ h2flag = 0
+}
+
+/^## (TODO|DOING|WAITING|MEETING|INACTIVE|NEXT|CANCELLED|DONE|WONTFIX)/ {
+ if (match($0, / \{#.*?\}.*$/) == 0) {
+ print "Missing ID for line " NR ":\n" $0
+ exitstatus = 1
+ }
+ id_with_prefix = substr($2, 0, length($2) - 1)
+ match(id_with_prefix, /^\w+-/)
+ id = substr(id_with_prefix, RLENGTH + 1)
+ if (id in arr) {
+ print "Duplicate ID: " id
+ exitstatus = 1
+ } else {
+ arr[id] = 1
+ }
+
+ split($0, l, " ")
+ h2status = l[2]
+ h2flag = 1
+ prevline = $0
+}
+
+
+/^# Scratch$/ {
+ exit exitstatus
+}
+' TODOs.md
diff --git a/aux/workflow/commonmark.sh b/aux/workflow/commonmark.sh
new file mode 100755
index 0000000..088d447
--- /dev/null
+++ b/aux/workflow/commonmark.sh
@@ -0,0 +1,48 @@
+#!/bin/sh
+set -eu
+
+while getopts 'N:t:l:H:' flag; do
+ case "$flag" in
+ N)
+ PROJECT_UC="$OPTARG"
+ ;;
+ t)
+ TITLE="$OPTARG"
+ ;;
+ l)
+ THE_LANG="$OPTARG"
+ ;;
+ H)
+ ALTERNATES="$OPTARG"
+ ;;
+ *)
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+assert_arg() {
+ if [ -z "$1" ]; then
+ echo "Missing $2" >&2
+ exit 2
+ fi
+}
+
+assert_arg "${PROJECT_UC:-}" '-N PROJECT_UC'
+assert_arg "${TITLE:-}" '-t TITLE'
+assert_arg "${THE_LANG:-}" '-l THE_LANG'
+assert_arg "${ALTERNATES:-}" '-H ALTERNATES'
+
+pandoc \
+ --toc \
+ --highlight-style pygments \
+ --toc-depth=2 \
+ -s \
+ --metadata title="$PROJECT_UC - $TITLE" \
+ --metadata "lang=$THE_LANG" \
+ -r commonmark \
+ -w html \
+ -H aux/workflow/favicon.html \
+ -H aux/workflow/style.css \
+ -H "$ALTERNATES"
diff --git a/aux/workflow/dist.sh b/aux/workflow/dist.sh
new file mode 100755
index 0000000..48a9d57
--- /dev/null
+++ b/aux/workflow/dist.sh
@@ -0,0 +1,112 @@
+#!/bin/sh
+set -eu
+
+PROJECT_UC=
+while getopts 'd:V:n:N:m:' flag; do
+ case "$flag" in
+ d)
+ DATE="$OPTARG"
+ ;;
+ V)
+ VVERSION="v$OPTARG"
+ ;;
+ n)
+ PROJECT="$OPTARG"
+ ;;
+ N)
+ PROJECT_UC="$OPTARG"
+ ;;
+ m)
+ MAILING_LIST="$OPTARG"
+ ;;
+ *)
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+assert_arg() {
+ if [ -z "$1" ]; then
+ echo "Missing $2" >&2
+ exit 2
+ fi
+}
+
+assert_arg "${DATE:-}" '-d DATE'
+assert_arg "${VVERSION:-}" '-V VERSION'
+assert_arg "${PROJECT:-}" '-n PROJECT'
+assert_arg "${MAILING_LIST:-}" '-m MAILING_LIST'
+
+if [ -z "${PROJECT_UC:-}" ]; then
+ PROJECT_UC="$PROJECT"
+fi
+
+
+if [ "$(git rev-parse --abbrev-ref HEAD)" != 'main' ]; then
+ echo 'Not on branch "main".' >&2
+ exit 1
+fi
+
+if git show "$VVERSION" 1>/dev/null 2>/dev/null; then
+ echo "Version '$VVERSION' already exists." >&2
+ exit 1
+fi
+
+if [ "v$(awk '/^VERSION *=/{print $3; exit}' Makefile)" != "$VVERSION" ]; then
+ echo "Version '$VVERSION' mismatch with \$(VERSION) in Makefile." >&2
+ echo 'Make sure to invoke this script with "make dist".' >&2
+ exit 1
+fi
+
+if ! printf '%s\n%s\n' "$(git tag)" "$VVERSION" | sort -nct. -k1 -k2 -k3; then
+ echo 'New tag is not bigger than existing ones.' >&2
+ exit 1
+fi
+
+if [ "$DATE" != "$(git log -1 --format=%cd --date=short HEAD)" ]; then
+ echo "Date '$DATE' is not up-to-date." >&2
+ exit 1
+fi
+
+if [ "$(awk '/^DATE *=/{print $3; exit}' Makefile)" != "$DATE" ]; then
+ echo "Date '$DATE' mismatch with \$(DATE) in Makefile." >&2
+ echo 'Make sure to invoke this script with "make dist".' >&2
+ exit 1
+fi
+
+if [ "Release $VVERSION" != "$(git log --format=%B -1 HEAD | head -n1)" ]; then
+ echo "Commit message isn't 'Release $VVERSION'." >&2
+ exit 1
+fi
+
+make clean
+make dev-check EXTRA_VERSION="$VVERSION"
+
+if ! (git diff --quiet && git diff --quiet --staged); then
+ echo 'Dirty repository.'
+ exit 1
+fi
+
+
+git tag "$VVERSION"
+sh aux/workflow/sign-tarballs.sh -n "$PROJECT"
+
+
+printf 'Publish version? [Y/n]: ' >&2
+read -r publish
+
+if [ "$publish" = 'n' ]; then
+ cat <<EOF >&2
+Now push the tag and the signature before pushing the commit:
+
+git push origin refs/notes/signatures/tar.gz -o skip-ci --no-verify
+git push --tags -o skip-ci --no-verify
+git push
+
+EOF
+else
+ git push origin refs/notes/signatures/tar.gz -o skip-ci --no-verify
+ git push --tags -o skip-ci --no-verify
+ git push
+fi
diff --git a/aux/workflow/favicon.html b/aux/workflow/favicon.html
new file mode 100644
index 0000000..8f9327c
--- /dev/null
+++ b/aux/workflow/favicon.html
@@ -0,0 +1 @@
+<link rel="icon" type="image/svg+xml" href="favicon.svg" />
diff --git a/aux/workflow/favicon.svg b/aux/workflow/favicon.svg
new file mode 100644
index 0000000..ce566b2
--- /dev/null
+++ b/aux/workflow/favicon.svg
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
+ <path d="M 0 8 L 1 8 L 1 9 L 0 9 L 0 8 Z" />
+ <path d="M 0 13 L 1 13 L 1 14 L 0 14 L 0 13 Z" />
+ <path d="M 1 8 L 2 8 L 2 9 L 1 9 L 1 8 Z" />
+ <path d="M 1 13 L 2 13 L 2 14 L 1 14 L 1 13 Z" />
+ <path d="M 2 8 L 3 8 L 3 9 L 2 9 L 2 8 Z" />
+ <path d="M 2 13 L 3 13 L 3 14 L 2 14 L 2 13 Z" />
+ <path d="M 3 8 L 4 8 L 4 9 L 3 9 L 3 8 Z" />
+ <path d="M 3 13 L 4 13 L 4 14 L 3 14 L 3 13 Z" />
+ <path d="M 4 7 L 5 7 L 5 8 L 4 8 L 4 7 Z" />
+ <path d="M 4 8 L 5 8 L 5 9 L 4 9 L 4 8 Z" />
+ <path d="M 4 13 L 5 13 L 5 14 L 4 14 L 4 13 Z" />
+ <path d="M 5 6 L 6 6 L 6 7 L 5 7 L 5 6 Z" />
+ <path d="M 5 7 L 6 7 L 6 8 L 5 8 L 5 7 Z" />
+ <path d="M 5 13 L 6 13 L 6 14 L 5 14 L 5 13 Z" />
+ <path d="M 6 5 L 7 5 L 7 6 L 6 6 L 6 5 Z" />
+ <path d="M 6 6 L 7 6 L 7 7 L 6 7 L 6 6 Z" />
+ <path d="M 6 14 L 7 14 L 7 15 L 6 15 L 6 14 Z" />
+ <path d="M 7 1 L 8 1 L 8 2 L 7 2 L 7 1 Z" />
+ <path d="M 7 14 L 8 14 L 8 15 L 7 15 L 7 14 Z" />
+ <path d="M 7 15 L 8 15 L 8 16 L 7 16 L 7 15 Z" />
+ <path d="M 7 2 L 8 2 L 8 3 L 7 3 L 7 2 Z" />
+ <path d="M 7 3 L 8 3 L 8 4 L 7 4 L 7 3 Z" />
+ <path d="M 7 4 L 8 4 L 8 5 L 7 5 L 7 4 Z" />
+ <path d="M 7 5 L 8 5 L 8 6 L 7 6 L 7 5 Z" />
+ <path d="M 8 1 L 9 1 L 9 2 L 8 2 L 8 1 Z" />
+ <path d="M 8 15 L 9 15 L 9 16 L 8 16 L 8 15 Z" />
+ <path d="M 9 1 L 10 1 L 10 2 L 9 2 L 9 1 Z" />
+ <path d="M 9 2 L 10 2 L 10 3 L 9 3 L 9 2 Z" />
+ <path d="M 9 6 L 10 6 L 10 7 L 9 7 L 9 6 Z" />
+ <path d="M 9 15 L 10 15 L 10 16 L 9 16 L 9 15 Z" />
+ <path d="M 10 2 L 11 2 L 11 3 L 10 3 L 10 2 Z" />
+ <path d="M 10 3 L 11 3 L 11 4 L 10 4 L 10 3 Z" />
+ <path d="M 10 4 L 11 4 L 11 5 L 10 5 L 10 4 Z" />
+ <path d="M 10 5 L 11 5 L 11 6 L 10 6 L 10 5 Z" />
+ <path d="M 10 6 L 11 6 L 11 7 L 10 7 L 10 6 Z" />
+ <path d="M 11 6 L 12 6 L 12 7 L 11 7 L 11 6 Z" />
+ <path d="M 11 8 L 12 8 L 12 9 L 11 9 L 11 8 Z" />
+ <path d="M 10 15 L 11 15 L 11 16 L 10 16 L 10 15 Z" />
+ <path d="M 11 10 L 12 10 L 12 11 L 11 11 L 11 10 Z" />
+ <path d="M 11 12 L 12 12 L 12 13 L 11 13 L 11 12 Z" />
+ <path d="M 11 14 L 12 14 L 12 15 L 11 15 L 11 14 Z" />
+ <path d="M 11 15 L 12 15 L 12 16 L 11 16 L 11 15 Z" />
+ <path d="M 12 6 L 13 6 L 13 7 L 12 7 L 12 6 Z" />
+ <path d="M 12 8 L 13 8 L 13 9 L 12 9 L 12 8 Z" />
+ <path d="M 12 10 L 13 10 L 13 11 L 12 11 L 12 10 Z" />
+ <path d="M 12 12 L 13 12 L 13 13 L 12 13 L 12 12 Z" />
+ <path d="M 12 14 L 13 14 L 13 15 L 12 15 L 12 14 Z" />
+ <path d="M 13 6 L 14 6 L 14 7 L 13 7 L 13 6 Z" />
+ <path d="M 13 8 L 14 8 L 14 9 L 13 9 L 13 8 Z" />
+ <path d="M 13 10 L 14 10 L 14 11 L 13 11 L 13 10 Z" />
+ <path d="M 13 12 L 14 12 L 14 13 L 13 13 L 13 12 Z" />
+ <path d="M 13 13 L 14 13 L 14 14 L 13 14 L 13 13 Z" />
+ <path d="M 13 14 L 14 14 L 14 15 L 13 15 L 13 14 Z" />
+ <path d="M 14 7 L 15 7 L 15 8 L 14 8 L 14 7 Z" />
+ <path d="M 14 8 L 15 8 L 15 9 L 14 9 L 14 8 Z" />
+ <path d="M 14 9 L 15 9 L 15 10 L 14 10 L 14 9 Z" />
+ <path d="M 14 10 L 15 10 L 15 11 L 14 11 L 14 10 Z" />
+ <path d="M 14 11 L 15 11 L 15 12 L 14 12 L 14 11 Z" />
+ <path d="M 14 12 L 15 12 L 15 13 L 14 13 L 14 12 Z" />
+</svg>
diff --git a/aux/workflow/l10n.sh b/aux/workflow/l10n.sh
new file mode 100755
index 0000000..cb132a3
--- /dev/null
+++ b/aux/workflow/l10n.sh
@@ -0,0 +1,109 @@
+#!/bin/sh
+set -eu
+
+LANGS=
+MAX_JOBS=64
+while getopts 'l:L:j:' flag; do
+ case "$flag" in
+ l)
+ LANGS="$OPTARG"
+ ;;
+ L)
+ CONTRIBLANGS="$OPTARG"
+ ;;
+ j)
+ MAX_JOBS="$OPTARG"
+ ;;
+ *)
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+assert_arg() {
+ if [ -z "$1" ]; then
+ echo "Missing $2" >&2
+ exit 2
+ fi
+}
+
+assert_arg "${LANGS:-}" '-l LANGS'
+
+PARALLEL_N=0
+parallel_run() {
+ {
+ "$@"
+ } &
+ PARALLEL_N=$((PARALLEL_N + 1))
+ if [ "$PARALLEL_N" = "$MAX_JOBS" ]; then
+ wait
+ PARALLEL_N=0
+ fi
+}
+
+po_run() {
+ from_f="$1"
+ lang="$2"
+ to_f="$(echo "$from_f" | sed "s/en\./$lang./")"
+ printf 'Generating %s...\n' "$to_f" >&2
+ pofile="po/LC_MESSAGES/$from_f/$lang.po"
+ mkdir -p "$(dirname "$pofile")"
+
+ case "$from_f" in
+ *.en.[1-9].in)
+ po4a-updatepo -f man -m "$from_f" -p "$pofile"
+ po4a-translate -f man -m "$from_f" \
+ -p "$pofile" -l "$to_f" -k 0 -v >&2
+ ;;
+ *.en.html)
+ po4a-updatepo -f xhtml -m "$from_f" -p "$pofile"
+ po4a-translate -f xhtml -m "$from_f" \
+ -p "$pofile" -l "$to_f" -k 0 -v >&2
+ ;;
+ *.en.md)
+ touch "$pofile"
+ md2po --include-codeblocks --quiet --save \
+ --po-filepath "$pofile" < "$from_f"
+ po2md --pofiles "$pofile" --save "$to_f" \
+ --quiet --wrapwidth 999 < "$from_f"
+ ;;
+ *.en.msg|*.en.txt)
+ po4a-updatepo -f text -m "$from_f" -p "$pofile"
+ po4a-translate -f text -m "$from_f" \
+ -p "$pofile" -l "$to_f" -k 0 -v >&2
+ ;;
+ *)
+ echo "Unsupported file format: $from_f" >&2
+ exit 2
+ ;;
+ esac
+}
+
+for from_f in "$@"; do
+ for lang in $LANGS ${CONTRIBLANGS:-}; do
+ parallel_run po_run "$from_f" "$lang"
+ done
+done
+
+EXIT_CODE=0
+
+end="\033[0m"
+yellowb="\033[1;33m"
+for lang in $LANGS; do
+ # shellcheck disable=2044
+ for pofile in $(find po/ -type f -name "$lang.po"); do
+ if ! LANG=POSIX msgfmt --statistics "$pofile" 2>&1 |
+ grep untranslated; then
+ continue
+ fi
+ # shellcheck disable=2059
+ printf "\n ${yellowb}WARNING${end}!" >&2
+ printf "\n Missing translations for %s\n\n" "$pofile" >&2
+ EXIT_CODE=1
+ done
+done
+
+if [ -n "${ASSERT_NO_MISSING_TRANSLATIONS:-}" ]; then
+ exit "$EXIT_CODE"
+fi
diff --git a/aux/workflow/preamble.md b/aux/workflow/preamble.md
new file mode 100644
index 0000000..a699d04
--- /dev/null
+++ b/aux/workflow/preamble.md
@@ -0,0 +1,16 @@
+# About
+
+TODOs for [@PROJECT_UC@](https://@TLD@/@PROJECT@/en/).
+
+Register a new one at
+<span id="new">[~euandreh/@MAILING_LIST@@lists.sr.ht](mailto:~euandreh/@MAILING_LIST@@lists.sr.ht?subject=%5B@PROJECT@%5D%20BUG%20or%20TASK%3A%20%3Cdescription%3E)</span>
+and see [existing discussions](https://lists.sr.ht/~euandreh/@MAILING_LIST@?search=%5B@PROJECT@%5D).
+
+*Você também pode escrever em português*.
+
+*Vous pouvez aussi écrire en français*.
+
+*Vi povas ankaŭ skribi esperante*.
+
+*Tu también puedes escribir en español*.
+
diff --git a/aux/workflow/public.sh b/aux/workflow/public.sh
new file mode 100755
index 0000000..2c8c36a
--- /dev/null
+++ b/aux/workflow/public.sh
@@ -0,0 +1,83 @@
+#!/bin/sh
+set -eu
+
+TLD="$(cat aux/tld.txt)"
+. aux/lib.sh
+
+PROJECT_UC=
+while getopts 'n:N:m:o:l:' flag; do
+ case "$flag" in
+ n)
+ PROJECT="$OPTARG"
+ ;;
+ N)
+ PROJECT_UC="$OPTARG"
+ ;;
+ m)
+ MAILING_LIST="$OPTARG"
+ ;;
+ o)
+ OUTDIR="$OPTARG"
+ ;;
+ l)
+ LANGS="$OPTARG"
+ ;;
+ *)
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+assert_arg() {
+ if [ -z "$1" ]; then
+ echo "Missing $2" >&2
+ exit 2
+ fi
+}
+
+assert_arg "${PROJECT:-}" '-n PROJECT'
+assert_arg "${MAILING_LIST:-}" '-m MAILING_LIST'
+assert_arg "${OUTDIR:-}" '-o OUTDIR'
+assert_arg "${LANGS:-}" '-l LANGS'
+PROJECT_UC="${PROJECT_UC:-$PROJECT}"
+
+
+alternates_for() {
+ ALTERNATES="$(mkstemp)"
+ lang="$1"
+ OUTNAME="$2"
+ for l in $LANGS; do
+ if [ "$l" = "$lang" ]; then
+ continue
+ fi
+ cat <<EOF >> "$ALTERNATES"
+<link rel="alternate" href="https://$TLD/$PROJECT/$l/$OUTNAME" hreflang="$l" />
+EOF
+ done
+ echo "$ALTERNATES"
+}
+
+
+mkdir -p "$OUTDIR"
+
+sh aux/workflow/TODOs.sh \
+ -N "$PROJECT_UC" -n "$PROJECT" -m "$MAILING_LIST" -o "$OUTDIR"
+
+for lang in $LANGS; do
+ mkdir -p "$OUTDIR/$lang/"
+ sh aux/workflow/commonmark.sh \
+ -N "$PROJECT" -l "$lang" -t README \
+ -H "$(alternates_for "$lang" '')" \
+ < "README.$lang.md" > "$OUTDIR/$lang/index.html"
+ sh aux/workflow/commonmark.sh \
+ -N "$PROJECT" -l "$lang" -t CHANGELOG \
+ -H "$(alternates_for "$lang" 'CHANGELOG.html')" \
+ < "CHANGELOG.$lang.md" > "$OUTDIR/$lang/CHANGELOG.html"
+ ln -fs ../favicon.svg "$OUTDIR/$lang"
+done
+
+ln -fs en/index.html "$OUTDIR/index.html"
+cp aux/workflow/favicon.svg "$OUTDIR"
+
+sh aux/ci/report.sh -n "$PROJECT" -o "$OUTDIR"
diff --git a/aux/workflow/sign-tarballs.sh b/aux/workflow/sign-tarballs.sh
new file mode 100755
index 0000000..3ab2bb8
--- /dev/null
+++ b/aux/workflow/sign-tarballs.sh
@@ -0,0 +1,38 @@
+#!/bin/sh
+set -eu
+
+while getopts 'n:' flag; do
+ case "$flag" in
+ n)
+ PROJECT="$OPTARG"
+ ;;
+ *)
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND -1))
+
+assert_arg() {
+ if [ -z "$1" ]; then
+ echo "Missing $2" >&2
+ exit 2
+ fi
+}
+
+assert_arg "${PROJECT:-}" '-n PROJECT'
+
+
+SIGNATURES="$(git notes --ref=refs/notes/signatures/tar.gz list | cut -d\ -f2)"
+for tag in $(git tag); do
+ COMMIT="$(git rev-list -n1 "$tag")"
+ if echo "$SIGNATURES" | grep -qF "$COMMIT"; then
+ continue
+ fi
+ echo "Adding missing signature to $tag" >&2
+ git notes --ref=refs/notes/signatures/tar.gz add -C "$(
+ git archive --format tar.gz --prefix "$PROJECT-$tag/" "$tag" |
+ gpg --output - --armor --detach-sign |
+ git hash-object -w --stdin
+ )" "$tag"
+done
diff --git a/aux/workflow/style.css b/aux/workflow/style.css
new file mode 100644
index 0000000..56e4712
--- /dev/null
+++ b/aux/workflow/style.css
@@ -0,0 +1,62 @@
+<style>
+ @media(prefers-color-scheme: dark) {
+ :root {
+ color: white;
+ background-color: black;
+ }
+
+ a {
+ color: hsl(211, 100%, 60%);
+ }
+
+ a:visited {
+ color: hsl(242, 100%, 80%);
+ }
+ }
+
+ body {
+ max-width: 800px;
+ margin: 0 auto 0 auto;
+ }
+
+ hr {
+ background-color: #ccc;
+ }
+
+ .header-anchor {
+ opacity: 0.5;
+ }
+
+ .tag {
+ font-family: monospace;
+ font-size: 70%;
+ background-color: lightgray;
+ color: black;
+ padding: 3px;
+ border-radius: 5px;
+ }
+
+ .TODO {
+ color: brown;
+ }
+
+ .DOING {
+ color: yellowgreen;
+ }
+
+ .WAITING, .MEETING {
+ color: orange;
+ }
+
+ .INACTIVE {
+ color: gray;
+ }
+
+ .NEXT {
+ color: red;
+ }
+
+ .CANCELLED, .DONE, .WONTFIX {
+ color: green;
+ }
+</style>