Files
docs/update_cli_docs.sh
T
bircni 55c3a392a1 feat: generate the gt command reference from gitea/cli
update_cli_docs.sh runs gt's own generator and lays the pages out for the
site: one directory per command group, front matter, titles, sidebar
order, fenced help text and rewritten links. --released succeeds while no
series is documented. A weekly workflow proposes updates like the runner's.

Signed-off-by: bircni <[email protected]>
2026-10-22 11:28:15 +02:00

316 lines
10 KiB
Bash
Executable File

#!/bin/bash
#
# Regenerates the generated pages of the gt (Gitea CLI) documentation from the
# gitea/cli sources: one command reference page per command and help topic.
#
# Usage:
#
# ./update_cli_docs.sh develop docs, from the main branch
# ./update_cli_docs.sh --released every documented release series
# ./update_cli_docs.sh <git ref> <dir> one ref into one directory
#
# --released needs no version list: the documented series are the
# cli-docs_versioned_docs/version-<series>/reference directories, and the tag
# each one is generated from is the newest stable v<series>.x tag of gitea/cli,
# looked up through the Gitea API. A new series is documented by running
# `make cut-version PRODUCT=cli VERSION=<series>`, nothing here has to be edited.
set -euo pipefail
CLI_REMOTE="${CLI_REMOTE:-https://gitea.com/gitea/cli.git}"
CLI_API="${CLI_API:-https://gitea.com/api/v1/repos/gitea/cli}"
VERSIONED_DOCS="cli-docs_versioned_docs"
SRC_DIR=".tmp/upstream-cli"
OUT_DIR="$PWD/.tmp/cli-generated"
TAGS_FILE=".tmp/cli-tags.txt"
MARKER='Generated by update_cli_docs.sh from the gitea/cli sources, do not edit.'
mkdir -p .tmp
# prints every tag name of the cli repository, one per line
list_cli_tags() {
local page=1 names
while :; do
names="$(curl --silent --show-error --fail \
"$CLI_API/tags?limit=50&page=$page" |
grep -o '"name"[[:space:]]*:[[:space:]]*"[^"]*"' | cut -d '"' -f 4)"
[ -n "$names" ] || return 0
printf '%s\n' "$names"
page=$((page + 1))
done
}
# latest_stable_tag <series>, e.g. "0.1" -> "v0.1.2", empty if the series has
# no release yet. Pre-release tags (v0.1.0-rc1) are ignored on purpose.
latest_stable_tag() {
local series="$1"
if [ ! -s "$TAGS_FILE" ]; then
list_cli_tags > "$TAGS_FILE"
fi
{ grep -E "^v${series//./\\.}(\.[0-9]+)+$" "$TAGS_FILE" || true; } |
sed 's/^v//' |
sort -t . -k1,1n -k2,2n -k3,3n |
tail -n 1 |
sed 's/^/v/'
}
# generate_pages <git ref>, writes gt's markdown reference to $OUT_DIR/reference
generate_pages() {
local ref="$1" format=sha1 head
# gitea/cli uses sha256 object names, which a fetch into a sha1 repository rejects
head="$(git ls-remote "$CLI_REMOTE" HEAD | awk '$2 == "HEAD" { print $1; exit }')"
[ "${#head}" -ne 64 ] || format=sha256
if [ -d "$SRC_DIR/.git" ] && [ "$(git -C "$SRC_DIR" rev-parse --show-object-format)" = "$format" ]; then
git -C "$SRC_DIR" remote set-url origin "$CLI_REMOTE"
else
rm -rf "$SRC_DIR"
git init --quiet --object-format="$format" "$SRC_DIR"
git -C "$SRC_DIR" remote add origin "$CLI_REMOTE"
fi
git -C "$SRC_DIR" fetch --quiet --depth 1 origin "$ref"
git -C "$SRC_DIR" checkout --quiet --detach FETCH_HEAD
rm -rf "$OUT_DIR"
(cd "$SRC_DIR" && go run ./tools/cli-docs --out "$OUT_DIR")
}
# Cobra names a page after the command path, gt_pr_review_list.md. On the site
# a command with subcommands becomes a directory with an index page, so the
# sidebar groups it, and the help topics, which gt.md does not list, go to help/.
# command path of a cobra page name: gt_pr_review_list.md -> "pr review list"
command_of() {
local name="${1%.md}"
name="${name#gt}"
name="${name#_}"
printf '%s\n' "${name//_/ }"
}
is_topic() {
[[ " $TOPICS " == *" $1 "* ]]
}
# page path of a command, relative to the reference directory
page_of() {
local command="$1"
if [ -z "$command" ]; then
echo index.md
elif is_topic "$command"; then
echo "help/$command.md"
elif grep -qx "$command .*" <<< "$COMMANDS_LIST"; then
echo "${command// //}/index.md"
else
echo "${command// //}.md"
fi
}
# sidebar position of a command among its siblings, in the alphabetical order of gt --help
position_of() {
local command="$1" parent
if [ -z "$command" ]; then
echo 0
return
fi
if is_topic "$command"; then
awk -v topic="$command" '$0 == topic { print NR }' <<< "$TOPICS_LIST"
return
fi
parent="${command% *}"
[ "$parent" != "$command" ] || parent=""
awk -v parent="$parent" -v command="$command" '
{
p = $0
if (!sub(/ [^ ]+$/, "", p)) p = ""
if (p == parent) {
count++
if ($0 == command) { print count; exit }
}
}' <<< "$COMMANDS_LIST"
}
# write_page <cobra page> <target directory>
write_page() {
local source="$1" target="$2" command page depth up title position description
command="$(command_of "$(basename "$source")")"
page="$(page_of "$command")"
depth="$(tr -cd / <<< "$page" | wc -c | tr -d ' ')"
up=""
for ((i = 0; i < depth; i++)); do up="../$up"; done
position="$(position_of "$command")"
# the line below the heading is the short description of the command
description="$(sed -n '3p' "$source")"
if [ -z "$command" ]; then
title="Command reference"
elif is_topic "$command"; then
title="gt help $command"
else
title="gt $command"
fi
mkdir -p "$target/$(dirname "$page")"
{
printf -- '---\nsidebar_position: %s\ndescription: "%s"\n---\n\n' "$position" "${description//\"/\\\"}"
printf '# %s\n\n<!-- %s -->\n\n' "$title" "$MARKER"
if [ -z "$command" ]; then
cat <<'EOF'
Every `gt` command and flag, generated from the `--help` output of gt's main branch
or release. Global flags work with every command; [gt help environment](help/environment.md)
describes them together with the environment variables gt reads.
EOF
fi
# drop the cobra heading, move the other headings one level up, fence the
# synopsis and point the links between pages at their new paths. The
# synopsis is help text written for a terminal: its tables are indented by
# two spaces and its placeholders, such as <url>, would be read as HTML.
COMMANDS_LIST="$COMMANDS_LIST" awk -v up="$up" -v topics="$TOPICS" '
BEGIN {
n = split(ENVIRON["COMMANDS_LIST"], list, "\n")
for (i = 1; i <= n; i++) {
c = list[i]
sub(/ [^ ]+$/, "", c)
if (c != list[i]) parents[c] = 1
}
m = split(topics, names, " ")
for (i = 1; i <= m; i++) topic[names[i]] = 1
}
function target(file, c, index_page) {
c = file
sub(/\.md$/, "", c)
sub(/^gt_?/, "", c)
gsub(/_/, " ", c)
if (c == "") return up "index.md"
if (c in topic) return up "help/" c ".md"
index_page = c in parents
gsub(/ /, "/", c)
return up c (index_page ? "/index.md" : ".md")
}
function flush_synopsis( first, last, i) {
first = 1
while (first <= lines && synopsis[first] == "") first++
last = lines
while (last >= first && synopsis[last] == "") last--
if (first <= last) {
print "```text"
for (i = first; i <= last; i++) print synopsis[i]
print "```"
print ""
}
in_synopsis = 0
}
NR == 1 && /^## / { skip = 1; next }
skip && /^$/ { skip = 0; next }
in_synopsis && !/^```/ && !/^### (Examples|Options|Options inherited from parent commands|SEE ALSO)$/ {
synopsis[++lines] = $0
next
}
in_synopsis { flush_synopsis() }
/^### Synopsis$/ { print "## Synopsis"; print ""; in_synopsis = 1; lines = 0; next }
/^```/ { fenced = !fenced }
!fenced && /^#+ / { sub(/^#/, "") }
/^\* \[/ { sub(/\)\t - /, "): ") }
{
line = $0
out = ""
while (match(line, /\]\(gt[^)]*\.md\)/)) {
file = substr(line, RSTART + 2, RLENGTH - 3)
out = out substr(line, 1, RSTART + 1) target(file) ")"
line = substr(line, RSTART + RLENGTH)
}
print out line
}
END { if (in_synopsis) flush_synopsis() }
' "$source"
} > "$target/$page"
}
# generate_reference <git ref> <target directory>
generate_reference() {
local ref="$1" target="$2" source command
generate_pages "$ref"
local reference="$OUT_DIR/reference"
if [ ! -s "$reference/gt.md" ]; then
echo "$CLI_REMOTE@$ref did not generate $reference/gt.md" >&2
exit 1
fi
# every command, in the alphabetical order gt --help lists them in
COMMANDS_LIST="$(for source in "$reference"/gt_*.md; do command_of "$(basename "$source")"; done | LC_ALL=C sort)"
# the help topics are the pages gt.md does not link to
TOPICS_LIST="$(for source in "$reference"/gt_*.md; do
command="$(command_of "$(basename "$source")")"
[[ "$command" == *" "* ]] && continue
grep -qF "](gt_$command.md)" "$reference/gt.md" || echo "$command"
done)"
TOPICS="$(tr '\n' ' ' <<< "$TOPICS_LIST")"
COMMANDS_LIST="$(grep -vxF -f <(printf '%s\n' $TOPICS) <<< "$COMMANDS_LIST" || true)"
# remove the pages of an earlier run, so a removed command does not linger
if [ -d "$target" ]; then
{ grep -rlF --include='*.md' "$MARKER" "$target" || true; } | while IFS= read -r source; do rm "$source"; done
find "$target" -depth -type d -empty -delete
fi
mkdir -p "$target"
write_page "$reference/gt.md" "$target"
for source in "$reference"/gt_*.md; do
write_page "$source" "$target"
done
if [ -n "$TOPICS" ]; then
{
printf -- '---\nsidebar_position: %s\ndescription: "%s"\n---\n\n' \
"$(($(grep -cv ' ' <<< "$COMMANDS_LIST") + 1))" \
"Guides built into gt, read with gt help <topic>."
printf '# Help topics\n\n<!-- %s -->\n\n' "$MARKER"
printf 'Guides built into gt, read with `gt help <topic>`:\n\n'
for command in $TOPICS; do
printf -- '- [gt help %s](%s.md): %s\n' "$command" "$command" "$(sed -n '3p' "$reference/gt_$command.md")"
done
} > "$target/help/index.md"
fi
echo "wrote $(grep -rlF --include='*.md' "$MARKER" "$target" | wc -l | tr -d ' ') pages to $target from $CLI_REMOTE@$ref"
}
# regenerates every documented release series from its newest stable tag
generate_released() {
local dir series tag found=0
for dir in "$VERSIONED_DOCS"/version-*/reference; do
[ -d "$dir" ] || continue
found=1
series="${dir#"$VERSIONED_DOCS"/version-}"
series="${series%/reference}"
tag="$(latest_stable_tag "$series")"
if [ -z "$tag" ]; then
echo "no stable v$series tag in $CLI_API, skipping $dir" >&2
continue
fi
generate_reference "$tag" "$dir"
done
# before the first release only the develop docs exist, which is not an error
if [ "$found" -eq 0 ]; then
echo "no $VERSIONED_DOCS/version-*/reference directory, no released series to regenerate"
fi
}
case "${1:-}" in
--released)
generate_released
;;
-h | --help)
sed -n '3,17s/^#\{1,2\} \{0,1\}//p' "$0"
;;
*)
generate_reference "${1:-main}" "${2:-cli-docs/reference}"
;;
esac