Files
wickra/.github/workflows/sync-about.yml
T
kingchenc a2ff35f5f9 ci(sync-about): auto-sync repo homepage + org profile from the indicator count (#84)
Extend the count-sync workflow to drive three more surfaces from the same
single count, so the org page never drifts again:

- Repo About **homepage** URL — enforced unconditionally via
  `gh repo edit --homepage` in the existing About step (fixes the stale
  kingchenc URL and self-heals). Uses the same Administration-write
  permission as --description, so NO extra token scope.
- Org **profile README** count (wickra-lib/.github, profile/README.md) —
  clone + sed + bot commit/push, same pattern as the Sync Wiki step.
- Org **description** field ('… N indicators, install-free.') — gh api PATCH.

The two org steps need ABOUT_SYNC_TOKEN scope the main-repo syncs do not
(write on wickra-lib/.github; admin:org for the description PATCH). Both are
written to soft-skip with a ::warning:: and continue-on-error, so the run
stays green until the scope is granted — then they sync with no further
change. Homepage swap-point to a docs domain is a one-line change.

Refs findings P9 / P10.
2026-05-31 03:02:32 +02:00

264 lines
12 KiB
YAML

name: Sync indicator count
# Indicator count appears in four places that must stay in sync with
# the number of public indicator types exported from
# crates/wickra-core/src/lib.rs (the `pub use indicators::{ ... }` block,
# minus the `FAMILIES` constant and any `*Output` companion structs):
#
# 1. README.md prose — synced on PR branches (this workflow)
# 2. GitHub repo "About" description — synced on push to main / v* tag
# 3. Wiki: Home.md / FAQ.md / Streaming-vs-Batch.md
# — synced on push to main / v* tag
# 4. site/index.md (local-only marketing site, not synced from CI)
# 5. org profile README count (wickra-lib/.github, profile/README.md)
# — synced on push to main / v* tag*
# 6. org description ("… N indicators, install-free.")
# — synced on push to main / v* tag*
#
# *Surfaces 5 + 6 need the ABOUT_SYNC_TOKEN to carry extra scope (write on
# wickra-lib/.github, and admin:org for the org-description PATCH). Until that
# scope is granted these two steps emit a ::warning:: and soft-skip — they
# never fail the run. The repo "About" homepage URL is also enforced in step 2
# (constant value, no extra scope) so it can never drift back to the old org.
#
# We count public types (not `mod xxx;` lines) because some modules export
# more than one indicator — e.g. `vwap.rs` exposes both `Vwap` and
# `RollingVwap`, so the mod-count under-reports by one. lib.rs is the
# single source of truth for what the bindings reach.
#
# Design: keep README in sync *before* a PR is merged, by pushing a
# fix-up commit to the PR head branch. After squash-merge into main
# the bot commit is folded into the single signed merge commit, so
# main's history never shows an unsigned "sync indicator count" entry.
#
# The push to PR head uses the default `GITHUB_TOKEN`, whose pushes
# explicitly do NOT trigger downstream workflows (anti-recursion
# policy). So a counter fix-up does not re-trigger ci.yml on the PR
# — it does, however, re-trigger sync-about.yml on the next PR
# `synchronize` event, which is what we want (a no-op if the counter
# is now correct).
on:
push:
branches: [main]
tags: ['v*']
pull_request:
types: [opened, synchronize, reopened]
workflow_dispatch:
# `contents: write` is needed so the workflow can push the counter
# fix-up commit to the PR head branch via the auto-provided
# GITHUB_TOKEN. The wider About / Wiki writes still go through the
# fine-grained PAT (ABOUT_SYNC_TOKEN) because they need
# `Administration: write` (gh repo edit) which GITHUB_TOKEN lacks.
permissions:
contents: write
pull-requests: read
jobs:
sync:
runs-on: ubuntu-latest
steps:
# On PRs from forks the head ref lives in another repo; pushing
# back to it from this workflow is blocked by GitHub. We still
# want the PR to surface the missing counter, so the check below
# falls back to a hard failure when push isn't possible.
- name: Determine if push to PR head is possible
id: ctx
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
if [ "${{ github.event.pull_request.head.repo.full_name }}" = "${{ github.repository }}" ]; then
echo "can_push=true" >> "$GITHUB_OUTPUT"
echo "head_ref=${{ github.event.pull_request.head.ref }}" >> "$GITHUB_OUTPUT"
else
echo "can_push=false" >> "$GITHUB_OUTPUT"
echo "head_ref=" >> "$GITHUB_OUTPUT"
fi
else
echo "can_push=false" >> "$GITHUB_OUTPUT"
echo "head_ref=" >> "$GITHUB_OUTPUT"
fi
# On PRs we check out the *head* commit (not the merge ref) so
# any fix-up commit we make goes onto the PR branch itself. On
# push events we check out the default ref. fetch-depth: 0 lets
# us push back without "shallow update not allowed".
- uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref }}
repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }}
# Default GITHUB_TOKEN is fine for the same-repo PR-branch
# push; the About / Wiki steps re-authenticate with the PAT
# below where needed.
- name: Count indicators
id: count
run: |
# Parse the `pub use indicators::{ ... }` block from lib.rs, strip
# the FAMILIES constant and any `*Output` companion structs, count
# the remaining identifiers. Pure-shell so the workflow doesn't
# require a python runtime.
n=$(sed -n '/^pub use indicators::{/,/^};/p' crates/wickra-core/src/lib.rs \
| tr ',{}' '\n' \
| sed 's/[[:space:]]//g' \
| grep -E '^[A-Z][A-Za-z0-9_]*$' \
| grep -vE '^FAMILIES$|Output$' \
| sort -u | wc -l)
echo "count=$n" >> "$GITHUB_OUTPUT"
echo "Indicator count: $n"
# ----- PR flow ---------------------------------------------------
- name: Check README counter (PR)
if: github.event_name == 'pull_request'
id: pr_check
run: |
n="${{ steps.count.outputs.count }}"
if grep -qE "^${n} streaming-first indicators" README.md; then
echo "matches=true" >> "$GITHUB_OUTPUT"
echo "README counter already at ${n}; nothing to do."
else
echo "matches=false" >> "$GITHUB_OUTPUT"
echo "README counter does not match ${n}; will fix up."
fi
- name: Fix counter on fork PR head (read-only, fail loud)
if: github.event_name == 'pull_request' && steps.pr_check.outputs.matches == 'false' && steps.ctx.outputs.can_push == 'false'
run: |
n="${{ steps.count.outputs.count }}"
echo "::error::README.md says a different indicator count than mod.rs (${n}). This PR is from a fork, so the workflow cannot push the fix; please update README.md to '${n} streaming-first indicators' and push again."
exit 1
- name: Patch README on PR head
if: github.event_name == 'pull_request' && steps.pr_check.outputs.matches == 'false' && steps.ctx.outputs.can_push == 'true'
id: pr_patch
run: |
n="${{ steps.count.outputs.count }}"
sed -i -E "s/[0-9]+ (streaming-first )?indicators/${n} \1indicators/g" README.md
if git diff --quiet; then
echo "No README changes after sed (counter regex did not match anything); skipping push."
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
- name: Commit & push counter fix to PR head
if: github.event_name == 'pull_request' && steps.pr_patch.outputs.changed == 'true'
run: |
git config user.name "wickra-bot"
git config user.email "wickra-bot@users.noreply.github.com"
git add README.md
git commit -m "chore: sync indicator count to ${{ steps.count.outputs.count }}"
git push origin "HEAD:${{ steps.ctx.outputs.head_ref }}"
# ----- main / tag flow ------------------------------------------
#
# After a PR squash-merges, this workflow runs again on the push
# to main. README is already correct (it was fixed on the PR
# branch before the merge); the only outward syncs left are the
# GitHub About description (repo metadata, not a commit) and the
# wiki repo (separate repo, no main history pollution). README is
# not touched on main any more.
- name: Update GitHub About (description + homepage)
if: github.event_name != 'pull_request'
env:
GH_TOKEN: ${{ secrets.ABOUT_SYNC_TOKEN }}
run: |
n="${{ steps.count.outputs.count }}"
# Canonical homepage. Swap to the docs domain (e.g.
# https://docs.wickra.org) here once the docs site is live — one place.
homepage="https://github.com/wickra-lib/wickra"
desc="Streaming-first technical indicators with a Rust core and Python, Node.js, and WebAssembly bindings. ${n} indicators, O(1) per-tick updates, no system dependencies. Drop-in TA-Lib replacement."
# Enforce the homepage unconditionally — it is a constant, so this both
# corrects the stale kingchenc URL and self-heals any future drift.
# Same Administration-write permission as --description (no extra scope).
gh repo edit --homepage "$homepage"
current=$(gh repo view --json description -q .description)
if [ "$current" = "$desc" ]; then
echo "About description unchanged; homepage enforced."
else
gh repo edit --description "$desc"
echo "About description + homepage updated."
fi
- name: Sync Wiki
if: github.event_name != 'pull_request'
env:
GH_TOKEN: ${{ secrets.ABOUT_SYNC_TOKEN }}
run: |
n="${{ steps.count.outputs.count }}"
git clone "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.wiki.git" wiki
cd wiki
sed -i -E "s/[0-9]+ (streaming-first )?indicators/${n} \1indicators/g" Home.md FAQ.md Streaming-vs-Batch.md
if git diff --quiet; then
echo "Wiki unchanged."
exit 0
fi
git config user.name "wickra-bot"
git config user.email "wickra-bot@users.noreply.github.com"
git add Home.md FAQ.md Streaming-vs-Batch.md
git commit -m "chore: sync indicator count to ${n}"
git push
# ----- org-profile sync (soft-skip until PAT scope lands) -------
#
# These two steps keep the org page (github.com/wickra-lib) in sync
# with the same count. They need ABOUT_SYNC_TOKEN scope the main-repo
# syncs do not: write on wickra-lib/.github, and admin:org for the org
# description PATCH. Both are written to soft-skip with a ::warning::
# (never fail the run) so this workflow stays green before the scope is
# granted — once it is, they start syncing with no further code change.
- name: Sync org profile README count
if: github.event_name != 'pull_request'
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.ABOUT_SYNC_TOKEN }}
run: |
n="${{ steps.count.outputs.count }}"
if ! git clone "https://x-access-token:${GH_TOKEN}@github.com/wickra-lib/.github.git" orgprofile 2>/dev/null; then
echo "::warning::cannot clone wickra-lib/.github — ABOUT_SYNC_TOKEN likely lacks write on that repo (findings P10.0a). Skipping org profile sync."
exit 0
fi
cd orgprofile
sed -i -E "s/[0-9]+ (streaming-first )?indicators/${n} \1indicators/g" profile/README.md
if git diff --quiet; then
echo "Org profile README count unchanged."
exit 0
fi
git config user.name "wickra-bot"
git config user.email "wickra-bot@users.noreply.github.com"
git add profile/README.md
git commit -m "chore: sync indicator count to ${n}"
if ! git push 2>/dev/null; then
echo "::warning::push to wickra-lib/.github failed — ABOUT_SYNC_TOKEN likely lacks write (findings P10.0a)."
else
echo "Org profile README synced to ${n}."
fi
- name: Sync org description
if: github.event_name != 'pull_request'
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.ABOUT_SYNC_TOKEN }}
run: |
n="${{ steps.count.outputs.count }}"
org="wickra-lib"
# Reading the org description is public; the PATCH needs admin:org.
current=$(gh api "orgs/${org}" --jq '.description // ""' 2>/dev/null || true)
if [ -z "$current" ]; then
echo "::warning::could not read org description (network/PAT?). Skipping."
exit 0
fi
updated=$(printf '%s' "$current" | sed -E "s/[0-9]+ indicators/${n} indicators/")
if [ "$current" = "$updated" ]; then
echo "Org description count unchanged."
exit 0
fi
if gh api -X PATCH "orgs/${org}" -f description="$updated" >/dev/null 2>&1; then
echo "Org description synced to ${n}."
else
echo "::warning::org description PATCH failed — ABOUT_SYNC_TOKEN likely lacks admin:org (findings P10.0b)."
fi