ci(docs): auto-sync PROJECT_TREE.md to the docs repo on push to main #16
54
.gitea/scripts/gen_tree.py
Normal file
54
.gitea/scripts/gen_tree.py
Normal file
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render an ASCII tree of tracked files, read from stdin (one path per line).
|
||||
|
||||
Used by the `sync-project-tree` workflow to regenerate this repo's PROJECT_TREE.md
|
||||
snapshot in the RunicGateway/docs repo. Feed it `git ls-files`:
|
||||
|
||||
git ls-files | python3 .gitea/scripts/gen_tree.py <root-label>
|
||||
|
||||
Deterministic ordering: directories before files, each group sorted
|
||||
case-insensitively with the raw name as a tiebreak. Output uses the classic
|
||||
`tree(1)` box-drawing style so the result is stable across runs and platforms.
|
||||
"""
|
||||
import sys
|
||||
|
||||
|
||||
def build(paths):
|
||||
root = {}
|
||||
for p in paths:
|
||||
p = p.strip().replace("\\", "/")
|
||||
if not p:
|
||||
continue
|
||||
node = root
|
||||
for part in p.split("/"):
|
||||
node = node.setdefault(part, {})
|
||||
return root
|
||||
|
||||
|
||||
def render(node, prefix, lines):
|
||||
entries = list(node.items())
|
||||
# directories (non-empty children dict) before files, then case-insensitive name
|
||||
entries.sort(key=lambda kv: (0 if kv[1] else 1, kv[0].lower(), kv[0]))
|
||||
for i, (name, child) in enumerate(entries):
|
||||
last = i == len(entries) - 1
|
||||
branch = "└── " if last else "├── "
|
||||
suffix = "/" if child else ""
|
||||
lines.append(f"{prefix}{branch}{name}{suffix}")
|
||||
if child:
|
||||
render(child, prefix + (" " if last else "│ "), lines)
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding="utf-8", newline="\n")
|
||||
except AttributeError:
|
||||
pass
|
||||
root_label = sys.argv[1] if len(sys.argv) > 1 else "."
|
||||
tree = build(sys.stdin.read().splitlines())
|
||||
lines = [f"{root_label}/"]
|
||||
render(tree, "", lines)
|
||||
sys.stdout.write("\n".join(lines) + "\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
111
.gitea/workflows/sync-project-tree.yml
Normal file
111
.gitea/workflows/sync-project-tree.yml
Normal file
@@ -0,0 +1,111 @@
|
||||
name: sync-project-tree
|
||||
|
||||
# Keeps this repo's file-layout snapshot (docs/link/PROJECT_TREE.md in the
|
||||
# RunicGateway/docs repo) current. On every push to `main` it regenerates the
|
||||
# tree from tracked files and, if it changed, opens (or force-updates) a pull
|
||||
# request against the docs repo. It never writes to the docs repo's `main`
|
||||
# directly. Auth reuses the same REGISTRY_USER / REGISTRY_TOKEN secrets the
|
||||
# other workflows use (the token needs repo read/write on RunicGateway/docs).
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch: {}
|
||||
|
||||
concurrency:
|
||||
group: sync-project-tree
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
GITEA_HOST: gitea.whitlocktech.com
|
||||
DOCS_REPO: RunicGateway/docs
|
||||
SELF_REPO: RunicGateway/link
|
||||
DOCS_PATH: link/PROJECT_TREE.md
|
||||
TREE_TITLE: uo-link
|
||||
ROOT_LABEL: link
|
||||
PR_BRANCH: chore/sync-link-tree
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out this repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Ensure python3 is available
|
||||
run: |
|
||||
set -euo pipefail
|
||||
command -v python3 >/dev/null 2>&1 || { sudo apt-get update -qq && sudo apt-get install -y -qq python3; }
|
||||
|
||||
- name: Render PROJECT_TREE.md from tracked files
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p _sync
|
||||
{
|
||||
printf '# %s — Project Tree\n\n' "${TREE_TITLE}"
|
||||
printf '> **Auto-generated.** This file is maintained by the `sync-project-tree` CI workflow in\n'
|
||||
printf '> the [`%s`](https://%s/%s) repository, which\n' "${SELF_REPO}" "${GITEA_HOST}" "${SELF_REPO}"
|
||||
printf '> opens a pull request here whenever the tracked file layout on `main` changes. Do not edit\n'
|
||||
printf '> by hand — changes will be overwritten by the next sync.\n\n'
|
||||
printf 'A snapshot of the tracked files in the repository (build output, dependencies, and other\n'
|
||||
printf 'git-ignored paths are excluded).\n\n'
|
||||
printf '```text\n'
|
||||
git ls-files | python3 .gitea/scripts/gen_tree.py "${ROOT_LABEL}"
|
||||
printf '```\n'
|
||||
} > _sync/PROJECT_TREE.md
|
||||
echo "----- generated ${DOCS_PATH} -----"
|
||||
cat _sync/PROJECT_TREE.md
|
||||
|
||||
- name: Open or update the docs PR if the tree changed
|
||||
env:
|
||||
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Secrets can carry a trailing CR/LF depending on how they were pasted;
|
||||
# strip line breaks before they land in a URL or Authorization header.
|
||||
CI_USER="$(printf '%s' "${REGISTRY_USER}" | tr -d '\r\n')"
|
||||
CI_TOKEN="$(printf '%s' "${REGISTRY_TOKEN}" | tr -d '\r\n')"
|
||||
API="https://${GITEA_HOST}/api/v1/repos/${DOCS_REPO}"
|
||||
REMOTE="https://${CI_USER}:${CI_TOKEN}@${GITEA_HOST}/${DOCS_REPO}.git"
|
||||
|
||||
git clone --depth 1 "${REMOTE}" docs_repo
|
||||
cd docs_repo
|
||||
git config user.name "runic-docs-bot"
|
||||
git config user.email "ci@whitlocktech.com"
|
||||
|
||||
mkdir -p "$(dirname "${DOCS_PATH}")"
|
||||
cp ../_sync/PROJECT_TREE.md "${DOCS_PATH}"
|
||||
git add "${DOCS_PATH}"
|
||||
if git diff --cached --quiet; then
|
||||
echo "PROJECT_TREE.md already up to date — nothing to sync."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
SHORT_SHA="$(echo "${GITHUB_SHA:-local}" | cut -c1-7)"
|
||||
git checkout -B "${PR_BRANCH}"
|
||||
git commit -m "docs(tree): sync ${DOCS_PATH} from ${SELF_REPO}@${SHORT_SHA} [skip ci]"
|
||||
git push --force "${REMOTE}" "HEAD:${PR_BRANCH}"
|
||||
|
||||
# Open a PR only if one isn't already open for this branch (a force-push
|
||||
# to an existing open PR's head updates it in place).
|
||||
OPEN="$(curl -sSf -H "Authorization: token ${CI_TOKEN}" \
|
||||
"${API}/pulls?state=open&limit=50" \
|
||||
| jq --arg b "${PR_BRANCH}" '[.[] | select(.head.ref == $b)] | length')"
|
||||
if [ "${OPEN}" = "0" ]; then
|
||||
curl -sSf -X POST "${API}/pulls" \
|
||||
-H "Authorization: token ${CI_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$(jq -n \
|
||||
--arg head "${PR_BRANCH}" \
|
||||
--arg base "main" \
|
||||
--arg title "docs(tree): sync ${DOCS_PATH}" \
|
||||
--arg body "Automated project-tree sync from [\`${SELF_REPO}\`](https://${GITEA_HOST}/${SELF_REPO}), regenerated from tracked files on \`main\`. Merge once the layout looks right; the workflow will keep this branch current until then." \
|
||||
'{head: $head, base: $base, title: $title, body: $body}')" \
|
||||
>/dev/null
|
||||
echo "Opened a new docs PR for ${PR_BRANCH}."
|
||||
else
|
||||
echo "Existing open docs PR for ${PR_BRANCH} was updated via force-push."
|
||||
fi
|
||||
Reference in New Issue
Block a user