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. Docs site: index.md / overview.md / Indicators-Overview.md # (wickra-lib/wickra-docs) — synced on push to main / v* tag* # 4. Marketing site count (wickra-lib/webpage: index.md / about.md / # .vitepress/config.ts) — push to main / v* tag* # 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* # 7. docs site published version (wickra-lib/wickra-docs: the # "Published versions" table in overview.md + the Rust quickstart prose) # — synced on v* tag only* # 8. Marketing site version (wickra-lib/webpage: api/*.md "Latest" lines, the # nav version label, and the wickra-wasm dep) — synced on v* tag only* # 9. Wiki pointer page count (wickra-lib/wickra.wiki, Home.md — the wiki was # collapsed to a single page that points at docs.wickra.org but still names # the count) — synced on push to main / v* tag* # # *Surfaces 3 + 7 need the ABOUT_SYNC_TOKEN to have write on # wickra-lib/wickra-docs, surfaces 4 + 8 on wickra-lib/webpage; surfaces 5 + 6 # need write on wickra-lib/.github and admin:org for the org-description PATCH; # surface 9 needs write on wickra-lib/wickra (the wiki rides on the parent # repo's permission). # Until that scope is granted these 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); it points at docs.wickra.org. # # Note: surface 7 carries the release *version*, not the indicator count, so # it is driven by the v* tag (which is the version) rather than the count. # # 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: on PRs this workflow is a READ-ONLY check. The indicator wiring # bumps both README.md and docs/README.md inside the author's code commit, so # the counter is already correct by the time CI runs. If it is not, the check # below fails loud and asks the author to re-run the wiring — it never pushes a # fix-up commit. # # (An earlier version pushed a GITHUB_TOKEN "sync indicator count" commit to # the PR head. Because GITHUB_TOKEN pushes trigger no workflows, that commit # moved the PR head onto a commit with no CI run, which hid the Codecov patch # status — keyed to the PR head sha — from the PR. Keeping the counter in the # code commit avoids that entirely.) on: push: branches: [main] tags: ['v*'] pull_request: types: [opened, synchronize, reopened] workflow_dispatch: # Least-privilege default for the auto-injected GITHUB_TOKEN. The `contents: # write` the workflow needs — to push the counter fix-up commit to the PR head # branch — is raised at the job level below, not here, so the top-level default # stays read-only (OpenSSF Scorecard: Token-Permissions). The wider About / # docs / webpage / org writes still go through the fine-grained PAT # (ABOUT_SYNC_TOKEN), which the `permissions:` key does not govern at all. permissions: contents: read pull-requests: read jobs: sync: runs-on: ubuntu-latest # This workflow never writes to wickra-lib/wickra with GITHUB_TOKEN: the PR # flow is a read-only check, and the main/tag flow writes only to other # repos (About metadata, docs, webpage, wiki, org) through the fine-grained # ABOUT_SYNC_TOKEN PAT. So GITHUB_TOKEN stays read-only (OpenSSF Scorecard: # Token-Permissions). permissions: contents: read pull-requests: read steps: # On PRs we check out the PR *head* commit (the author's code, not the # merge ref) so the counter check validates exactly what will land. On # push events we check out the default ref. No push is made, so a shallow # checkout is enough. # # Check out by head SHA, not head ref (branch name): a fast `gh pr merge # --squash --delete-branch` deletes the head branch the moment the PR # merges, often before this queued read-only check reaches its checkout. # Fetching the now-gone `refs/heads/` then fails the run (exit 1). # The head SHA stays reachable via `refs/pull/N/head` after the branch is # gone, so the checkout — and the run — survives an instant merge. - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 1 ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }} repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }} - 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, read-only) if: github.event_name == 'pull_request' run: | n="${{ steps.count.outputs.count }}" ok=true if ! grep -qE "^${n} streaming-first indicators" README.md; then echo "::error::README.md does not say '${n} streaming-first indicators' — lib.rs exports ${n}. Re-run the indicator wiring (it bumps README.md), then push again." ok=false fi if ! grep -qE "\*\*${n} indicators\*\*" docs/README.md; then echo "::error::docs/README.md does not say '**${n} indicators**' — lib.rs exports ${n}. Re-run the indicator wiring (it bumps docs/README.md), then push again." ok=false fi if [ "$ok" = "true" ]; then echo "README.md + docs/README.md counter already at ${n}; nothing to do." else exit 1 fi # ----- main / tag flow ------------------------------------------ # # After a PR squash-merges, this workflow runs again on the push to main. # README.md / docs/README.md are already correct (the indicator wiring # bumped them in the merged code commit); the only outward syncs left are # the GitHub About description (repo metadata, not a commit) and the docs / # webpage / wiki / org repos (separate repos, no main history pollution). # The wickra repo's own 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 — the docs site (P8.3). This is enforced on every # run, so it must only point at docs.wickra.org once that domain is # actually live (Cloudflare Pages, P8.1); merging this PR is therefore # gated on the domain resolving, otherwise the About link would 404. homepage="https://docs.wickra.org" desc="Streaming-first technical indicators with a Rust core and Python, Node.js, WebAssembly, C ABI, .NET, Go, Java, and R 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 # Counter sync target moved from the retired GitHub wiki to the docs site # repo (wickra-lib/wickra-docs). The count appears in index.md (hero), # overview.md prose, and Indicators-Overview.md prose. Soft-skips like the # org steps so a token/scope gap never fails the run. Uses its own clone # dir (docs-count) so it cannot collide with the tag-only version step # below, which clones the same repo into `docs`. - name: Sync docs indicator count (wickra-docs) 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/wickra-docs.git" docs-count 2>/dev/null; then echo "::warning::cannot clone wickra-lib/wickra-docs — ABOUT_SYNC_TOKEN likely lacks write on that repo (findings P10.0a). Skipping docs count sync." exit 0 fi cd docs-count sed -i -E "s/[0-9]+ (streaming-first )?indicators/${n} \1indicators/g" index.md overview.md Indicators-Overview.md .vitepress/config.ts if git diff --quiet; then echo "Docs indicator count unchanged." exit 0 fi git config user.name "wickra-bot" git config user.email "wickra-bot@users.noreply.github.com" git add index.md overview.md Indicators-Overview.md .vitepress/config.ts git commit -m "chore: sync indicator count to ${n}" if ! git push 2>/dev/null; then echo "::warning::push to wickra-lib/wickra-docs failed — ABOUT_SYNC_TOKEN likely lacks write (findings P10.0a)." else echo "Docs indicator count synced to ${n}." fi # The GitHub wiki (wickra-lib/wickra.wiki) was collapsed to a single # Home.md pointer page that sends visitors to docs.wickra.org, but that # page still names the count ("… for all N indicators"), so keep it in # sync here too. Mirrors the docs/webpage count steps: own clone dir # (wiki-count) and the same soft-skip contract. Wiki write rides on the # parent repo's permission, so the PAT needs write on wickra-lib/wickra; # the wiki has no signing gate, so a plain wickra-bot commit is fine. - name: Sync wiki pointer indicator count (wickra.wiki) 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/wickra.wiki.git" wiki-count 2>/dev/null; then echo "::warning::cannot clone wickra-lib/wickra.wiki — ABOUT_SYNC_TOKEN likely lacks write on the wiki. Skipping wiki count sync." exit 0 fi cd wiki-count sed -i -E "s/[0-9]+ (streaming-first )?indicators/${n} \1indicators/g" Home.md if git diff --quiet; then echo "Wiki pointer indicator count unchanged." exit 0 fi git config user.name "wickra-bot" git config user.email "wickra-bot@users.noreply.github.com" git add Home.md git commit -m "chore: sync indicator count to ${n}" if ! git push 2>/dev/null; then echo "::warning::push to wickra-lib/wickra.wiki failed — ABOUT_SYNC_TOKEN likely lacks write on the wiki." else echo "Wiki pointer indicator count synced to ${n}." fi # ----- 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 # ----- docs version sync (tag-only, soft-skip until PAT scope lands) ----- # # Surface 7: the docs site (wickra-lib/wickra-docs) carries the published # version in the "Published versions" table (overview.md) and the Rust # quickstart prose. Unlike the indicator count these change only on a # release, so this step runs on v* tag pushes only and takes the version # straight from the tag. It needs ABOUT_SYNC_TOKEN to have write on # wickra-lib/wickra-docs (findings P10.0a). Until that scope is granted it # soft-skips with a ::warning:: and never fails the run; once granted, every # release self-heals the docs version with no code change (replaces the old # manual P0.5 post-release wiki bump). - name: Sync docs version (wickra-docs) if: startsWith(github.ref, 'refs/tags/v') continue-on-error: true env: GH_TOKEN: ${{ secrets.ABOUT_SYNC_TOKEN }} run: | version="${GITHUB_REF#refs/tags/v}" if ! printf '%s' "$version" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then echo "::warning::tag '${GITHUB_REF}' is not a plain vMAJOR.MINOR.PATCH release; skipping docs version sync." exit 0 fi # Clone into `docs-ver`, NOT `docs`: on a tag push this job checks out # the wickra repo at the workspace root, which already contains a # top-level `docs/` directory, so `git clone … docs` fails with # "destination path 'docs' already exists" — silently, because of the # 2>/dev/null below — and the version sync never runs (this is exactly # why v0.4.0 did not bump the docs table). `docs-ver` mirrors the # `docs-count` dir used by the count step above and collides with # nothing in the repo. if ! git clone "https://x-access-token:${GH_TOKEN}@github.com/wickra-lib/wickra-docs.git" docs-ver 2>/dev/null; then echo "::warning::cannot clone wickra-lib/wickra-docs — ABOUT_SYNC_TOKEN likely lacks write on that repo (findings P10.0a). Skipping docs version sync." exit 0 fi cd docs-ver # Published-versions table rows (all registries): replace only the # version number, leaving the trailing padding + pipe intact. The # registry name is matched whether plain or wrapped in a markdown link # (\[?...\]?). The '.' in the quickstart pattern matches the literal # backtick around the version without needing a backtick in this shell # string. Historical "since X.Y.Z" references contain no such anchor and # are never matched. sed -i -E "s/^(\| \[?(crates\.io|PyPI|npm|NuGet|Maven Central|Go|r-universe)\]?.*\| )[0-9]+\.[0-9]+\.[0-9]+/\1${version}/" overview.md sed -i -E "s/(published crate is at version .)[0-9]+\.[0-9]+\.[0-9]+/\1${version}/" Quickstart-Rust.md # Quickstart-Java is the only quickstart whose install block pins a # version (the Maven `` tag and the `org.wickra:wickra:` # Gradle coordinate). Java publishes on every release, so it tracks the # same ${version}. sed -i -E "s|[0-9]+\.[0-9]+\.[0-9]+|${version}|" Quickstart-Java.md sed -i -E "s|(org\.wickra:wickra:)[0-9]+\.[0-9]+\.[0-9]+|\1${version}|" Quickstart-Java.md if git diff --quiet; then echo "Docs version already at ${version}." exit 0 fi git config user.name "wickra-bot" git config user.email "wickra-bot@users.noreply.github.com" git add overview.md Quickstart-Rust.md Quickstart-Java.md git commit -m "chore: sync published version to ${version}" if ! git push 2>/dev/null; then echo "::warning::push to wickra-lib/wickra-docs failed — ABOUT_SYNC_TOKEN likely lacks write (findings P10.0a)." else echo "Docs version synced to ${version}." fi # ----- webpage (marketing site) self-update (findings P12.1) ------------ # # The marketing site (wickra-lib/webpage) carries the same indicator count # and published version as the docs. Mirrors the docs steps above: the # count syncs on push-to-main + tag, the version syncs on v* tags only. # Distinct clone dirs (webpage-count / webpage-ver) avoid any collision on # a tag run. Soft-skips with a ::warning:: if the token can't reach the # repo, so the run never fails. - name: Sync webpage indicator count (wickra-lib/webpage) 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/webpage.git" webpage-count 2>/dev/null; then echo "::warning::cannot clone wickra-lib/webpage — ABOUT_SYNC_TOKEN likely lacks write on that repo (findings P10.0a). Skipping webpage count sync." exit 0 fi cd webpage-count sed -i -E "s/[0-9]+ (streaming-first )?indicators/${n} \1indicators/g" index.md about.md .vitepress/config.ts if git diff --quiet; then echo "Webpage indicator count unchanged." exit 0 fi git config user.name "wickra-bot" git config user.email "wickra-bot@users.noreply.github.com" git add index.md about.md .vitepress/config.ts git commit -m "chore: sync indicator count to ${n}" if ! git push 2>/dev/null; then echo "::warning::push to wickra-lib/webpage failed — ABOUT_SYNC_TOKEN likely lacks write (findings P10.0a)." else echo "Webpage indicator count synced to ${n}." fi - name: Sync webpage version (wickra-lib/webpage) if: startsWith(github.ref, 'refs/tags/v') continue-on-error: true env: GH_TOKEN: ${{ secrets.ABOUT_SYNC_TOKEN }} run: | version="${GITHUB_REF#refs/tags/v}" if ! printf '%s' "$version" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then echo "::warning::tag '${GITHUB_REF}' is not a plain vMAJOR.MINOR.PATCH release; skipping webpage version sync." exit 0 fi # The webpage pins wickra-wasm to the released version in package.json, # and its Cloudflare Pages build runs `npm clean-install`. release.yml # publishes wickra-wasm to npm in parallel on this same tag and finishes # minutes later, so committing the bump immediately would point the site # at a version npm cannot resolve yet (ETARGET) and break the build — # exactly what happened on v0.4.0. Wait until wickra-wasm@$version is # actually live on npm before committing; if it never appears (the wasm # publish failed), skip rather than push a build-breaking commit. echo "Waiting for wickra-wasm@${version} on npm before bumping the webpage..." attempts=0 until npm view "wickra-wasm@${version}" version >/dev/null 2>&1; do attempts=$((attempts + 1)) if [ "$attempts" -ge 30 ]; then echo "::warning::wickra-wasm@${version} not on npm after ~15 min; skipping webpage version sync to avoid a broken Cloudflare build." exit 0 fi echo " not on npm yet (attempt ${attempts}/30); waiting 30s..." sleep 30 done echo "wickra-wasm@${version} is live on npm; proceeding with the webpage version bump." if ! git clone "https://x-access-token:${GH_TOKEN}@github.com/wickra-lib/webpage.git" webpage-ver 2>/dev/null; then echo "::warning::cannot clone wickra-lib/webpage — ABOUT_SYNC_TOKEN likely lacks write (findings P10.0a). Skipping webpage version sync." exit 0 fi cd webpage-ver # api/*.md "Latest" lines, the nav version label, and the wickra-wasm # dep pin. The '.' anchors match the backtick / quote / caret without a # literal in this shell string; historical "Since X.Y.Z" prose has no # such anchor and is never matched. sed -i -E "s/(Latest:\*\* \[.wickra(-wasm)? )[0-9]+\.[0-9]+\.[0-9]+/\1${version}/" api/*.md sed -i -E "s/(text: .v)[0-9]+\.[0-9]+\.[0-9]+/\1${version}/" .vitepress/config.ts sed -i -E "s/(.wickra-wasm.: .\^)[0-9]+\.[0-9]+\.[0-9]+/\1${version}/" package.json # Java is the only target whose install snippet pins a version: the # Maven `` tag in the api/java.md dependency block and the # home-page install tab in index.md, plus any `org.wickra:wickra:` # Gradle coordinate. Java publishes on every release, so it tracks the # same ${version} as the rest. (Other languages' api/*.md carry a # "Latest:" line handled above; Java uses the XML snippet instead.) sed -i -E "s|[0-9]+\.[0-9]+\.[0-9]+|${version}|" api/java.md index.md sed -i -E "s|(org\.wickra:wickra:)[0-9]+\.[0-9]+\.[0-9]+|\1${version}|" api/java.md index.md # Keep package-lock.json in sync with the package.json bump. The site's # Cloudflare build runs `npm clean-install` (npm ci), which hard-fails # with EUSAGE if the lockfile still pins the previous wickra-wasm — # editing package.json alone is not enough. The npm-wait above already # proved wickra-wasm@$version is resolvable, so --package-lock-only # regenerates the lock (version + resolved + integrity) without fetching # node_modules. Guard it: if the regen fails, skip the whole commit so we # never push a package.json/lock mismatch that would break the build. if ! npm install --package-lock-only --no-audit --no-fund; then echo "::warning::could not regenerate package-lock.json for wickra-wasm@${version}; skipping webpage version sync to avoid a lockfile-drift build break." exit 0 fi if git diff --quiet; then echo "Webpage version already at ${version}." exit 0 fi git config user.name "wickra-bot" git config user.email "wickra-bot@users.noreply.github.com" git add api/*.md index.md .vitepress/config.ts package.json package-lock.json git commit -m "chore: sync published version to ${version}" if ! git push 2>/dev/null; then echo "::warning::push to wickra-lib/webpage failed — ABOUT_SYNC_TOKEN likely lacks write (findings P10.0a)." else echo "Webpage version synced to ${version}." fi