refactor: Decompose program-lib.sh into portable modules (#35) - #50
refactor: Decompose program-lib.sh into portable modules (#35)#50rubambiza wants to merge 14 commits into
Conversation
Design for splitting the ~985-line program-lib.sh into four balanced, self-contained, bash-3.2-safe modules (core/github-api/fork/org) behind an unchanged aggregator entrypoint, plus a portability contract and a Linux+macOS CI matrix. Pure refactor; positioned so the agent-skills vendor copies (#2149) get clean modular files. Module boundaries are data-driven (function-usage audit across the 9 consumers); flat layout matches how agent-skills already vendors scripts; self-sourcing modules with load-once guards keep subset-copying safe. Assisted-By: Claude Code (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Gloire Rubambiza <gloire@ibm.com>
Captures the 23-function public surface of program-lib.sh so the decomposition refactor (rossoctl#35) can prove no function is lost or renamed as functions move into modules. Assisted-By: Claude Code (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Gloire Rubambiza <gloire@ibm.com>
Move workspace/date/json/diff/report helpers into scripts/core.sh with a load-once guard; program-lib.sh sources it. Verbatim move, no behavior change; inventory test green. Assisted-By: Claude Code (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Gloire Rubambiza <gloire@ibm.com>
Move the gh backoff wrapper and issue read/close/pr-check helpers into scripts/github-api.sh with a load-once guard. Verbatim move, no behavior change; inventory test green. Assisted-By: Claude Code (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Gloire Rubambiza <gloire@ibm.com>
Move fork/PR creation, issue-field validation, and candidate scoring into scripts/fork.sh with a load-once guard. These helpers reference no other module (gh is called directly), so fork.sh is guard-only. Verbatim move, no behavior change; inventory test green. Assisted-By: Claude Code (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Gloire Rubambiza <gloire@ibm.com>
Move org-identity + core-repo helpers into scripts/org.sh (guard-only, no cross-module deps). program-lib.sh is now a thin aggregator sourcing the four modules; the entrypoint the 9 consumers use is unchanged. Also splits an unparseable inline shellcheck directive on canonical_repo_for_dir into a comment line plus a clean directive (same effect). Verbatim move, no behavior change; inventory test + full suite green. Assisted-By: Claude Code (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Gloire Rubambiza <gloire@ibm.com>
Proves each library module sources on its own (self-sourcing guards + relative dep resolution) and defines its functions -- the property that makes a module safe to vendor as a subset. Assisted-By: Claude Code (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Gloire Rubambiza <gloire@ibm.com>
link-health-scanner used an associative array (ISSUE_COUNTS), which fails on macOS's default bash 3.2. Replace with a newline-delimited repo<TAB>count accumulator plus _ic_get/_ic_incr helpers. Verified count parity (3/1/0) and no bash-4 constructs remain; no output change. Assisted-By: Claude Code (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Gloire Rubambiza <gloire@ibm.com>
Add a test workflow with an ubuntu-latest + macos-latest matrix. The macOS leg exercises BSD coreutils and the bash-3.2 floor, enforcing the portability contract (catches declare -A / mapfile / GNU-only flags). shellcheck targets the five library modules at warning severity; broader consumer-script linting is tracked as a separate follow-up. Pins actions/checkout to a SHA with least-privilege permissions. Assisted-By: Claude Code (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Gloire Rubambiza <gloire@ibm.com>
Add a README section describing the four library modules, the aggregator entrypoint, and the bash-3.2+ portability contract enforced by CI. Update the shared-helpers note to point at the module split. Assisted-By: Claude Code (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Gloire Rubambiza <gloire@ibm.com>
clawgenti
left a comment
There was a problem hiding this comment.
Decomposes program-lib.sh into four focused, vendorable modules (core.sh, github-api.sh, fork.sh, org.sh) behind a thin aggregator, fixes the declare -A bash-4 portability blocker in link-health-scanner.sh, and adds a matrix CI workflow (Linux + macOS) with a function-inventory equivalence test. The design is well-executed with load-once guards, correct $BASH_SOURCE-relative dep resolution, and thorough inline documentation.
Finding: fork.sh is missing the self-sourcing of core.sh and github-api.sh specified in the design document (§ "Dependency loading", lines 89–97), which shows those . source lines as part of the module header. The current implementation works because fork.sh’s functions only invoke gh/git directly rather than calling gh_with_backoff or other library helpers — but the discrepancy between spec and implementation may cause confusion for anyone vendoring fork.sh under the assumption it is self-contained with respect to its stated deps. Either add the source lines (matching the spec) or update the design doc to reflect that fork.sh intentionally has no intra-library dependencies.
Reviewed by clawgenti using the github-pr-review skill
| # ## Portability | ||
| # Targets bash 3.2+ (macOS default) through modern bash. | ||
| [ -n "${_FORK_SH_LOADED:-}" ] && return | ||
| _FORK_SH_LOADED=1 |
There was a problem hiding this comment.
The design doc (§ Dependency loading) shows fork.sh sourcing core.sh and github-api.sh here:
_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
. "$_LIB_DIR/core.sh"
. "$_LIB_DIR/github-api.sh"The current implementation omits these — which is fine because none of fork.sh’s functions actually call gh_with_backoff or other library helpers (they use raw gh/git directly). But it leaves the spec and the code out of sync. Suggest either adding the source lines to match the spec, or adding a # No intra-library deps comment so the omission is intentional and documented for vendoring consumers.
There was a problem hiding this comment.
Went with your second suggestion — added a "No intra-library deps" header comment to fork.sh (and to github-api.sh and org.sh for consistency), so the intentional omission is documented right where a vendoring consumer reads it (commit 0df0db5). fork.sh also notes the self-source pattern to add if a function later calls a core.sh/github-api.sh helper. The design doc was corrected in a58ea67 to match. Comment-only; inventory (23 fns) and module smoke tests still pass.
Assisted-By: Claude Code
The design doc §"Dependency loading" showed fork.sh (and by category github-api.sh, org.sh) self-sourcing core.sh/github-api.sh. The implementation deliberately deviated: empirical measurement of each module's function bodies found no cross-module helper calls, so all four modules are guard-only with no source lines. Update the spec to reflect the implemented, measured reality — modules self-source only dependencies they actually reference, and the current split has none — so a vendored fork.sh is genuinely self-contained and no module declares an unused dependency. Resolves the PR rossoctl#50 review finding. Assisted-By: Claude Code (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Gloire Rubambiza <gloire@ibm.com>
clawgenti
left a comment
There was a problem hiding this comment.
Solid refactor: clean module split with load-once guards, bash-3.2 portability fix for declare -A, equivalence verified by function-inventory + smoke tests, and CI green on both Linux and macOS.
docs/specs/…(latest commit): The CI section says "runshellcheckoverscripts/*.sh" but.github/workflows/tests.ymltargets only the five library modules (scripts/core.sh scripts/github-api.sh scripts/fork.sh scripts/org.sh scripts/program-lib.sh), not the full glob. Minor doc/code discrepancy — the narrower scope is intentional (consumer-script SC2034 tracked in #52), so the spec should say "library modules" rather thanscripts/*.sh.
Reviewed by clawgenti using the github-pr-review skill
|
|
||
| - **Matrix:** `ubuntu-latest` + `macos-latest`. The macOS runner exercises BSD `date`/`sed`/`grep` | ||
| and an older-bash environment, catching the `declare -A` class of defect automatically. | ||
| - **Steps:** run every `tests/*.sh`; run `shellcheck` over `scripts/*.sh`. |
There was a problem hiding this comment.
The CI step shellchecks only the five library modules (not scripts/*.sh) — consumer-script linting is intentionally deferred to #52. Consider updating this line to say "library modules" rather than scripts/*.sh to match what the workflow actually does.
Add a header comment to fork.sh, github-api.sh, and org.sh stating each sources no sibling module (functions invoke gh/git/jq/builtins directly), so a vendoring consumer can copy any one alone. fork.sh also documents the self-source pattern to add if a real cross-module call is later introduced; org.sh notes load_org_profile sources a runtime data file, not a module. Documents the omission in the code itself, where a vendoring consumer reads it — complementing the spec correction in a58ea67. Comment-only: inventory (23 fns) and module smoke tests pass, lib shellcheck clean at warning. Addresses the PR rossoctl#50 inline review thread on scripts/fork.sh. Assisted-By: Claude Code (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Gloire Rubambiza <gloire@ibm.com>
clawgenti
left a comment
There was a problem hiding this comment.
Clean, well-scoped refactor: the monolith decomposition is mechanically correct, equivalence guards are solid, and the declare -A portability fix is the right call.
docs/specs/…-design.mdstatus field is stale — line 4 readsDesign (awaiting review)but the work is fully implemented in this PR; should be updated toImplemented(or similar) so the doc stays trustworthy as a reference._ic_get/_ic_incrleak into global namespace — the helpers are defined at script top-level inlink-health-scanner.sh, not scoped inside a block. The_-prefix reduces collision risk but they will remain in the environment after the scanner exits (relevant if the script is eversourced rather than executed). Consider a_lhs_ic_get/_lhs_ic_incrprefix or wrapping them in a subshell block if isolation matters.
Reviewed by clawgenti using the github-pr-review skill
| # program-lib.sh Decomposition Design — Portable, Vendorable Modules | ||
|
|
||
| **Date:** 2026-08-09 | ||
| **Status:** Design (awaiting review) |
There was a problem hiding this comment.
Status is stale — this work is implemented in the PR itself. Consider updating to Implemented (or Implemented — 2026-08-09) so the doc remains trustworthy as a historical reference.
There was a problem hiding this comment.
Fixed in e442270 — Status is now "Implemented — 2026-08-09".
Assisted-By: Claude Code
| # owner/name (no whitespace), so tab-splitting is unambiguous. | ||
| ISSUE_COUNTS="" # rows of "repo<TAB>count" | ||
|
|
||
| _ic_get() { # $1=repo -> prints its count, or 0 if absent |
There was a problem hiding this comment.
_ic_get (and _ic_incr below) are defined at top level, so they leak into the calling environment if this script is ever sourced. Low risk today since it is executed, but consider a more namespaced prefix (e.g. _lhs_ic_get) to make intent clear.
There was a problem hiding this comment.
Done in 738529b — renamed to _lhs_ic_get / _lhs_ic_incr (all 5 sites). Pure token rename, no behavior change; bash -n clean, shellcheck clean on the changed lines, inventory + module + extract-broken-links tests pass.
Assisted-By: Claude Code
Two review-flagged staleness fixes in the design doc: - Status: "Design (awaiting review)" -> "Implemented — 2026-08-09" (the work ships in this PR). - CI step: the workflow shellchecks the five library modules, not scripts/*.sh; consumer-script linting is deferred to rossoctl#52. Spec text now says so, matching what tests.yml actually runs. Addresses two PR rossoctl#50 inline review threads. Assisted-By: Claude Code (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Gloire Rubambiza <gloire@ibm.com>
Rename the top-level _ic_get/_ic_incr accumulator helpers to _lhs_ic_get/_lhs_ic_incr. The script is executed (not sourced) so there is no live collision, but the namespaced prefix signals intent and avoids leaking generic names into a callers environment if it is ever sourced. Pure token rename (all 5 sites), no behavior change: bash -n clean, shellcheck clean on the changed lines, inventory + module + extract-broken- links tests pass. Addresses a PR rossoctl#50 inline review thread on link-health-scanner.sh. Assisted-By: Claude Code (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Gloire Rubambiza <gloire@ibm.com>
clawgenti
left a comment
There was a problem hiding this comment.
Clean modular decomposition of program-lib.sh into four focused shell modules behind an unchanged aggregator entrypoint, with equivalence-verified tests and a macOS/bash-3.2 portability fix. All checks pass. Ready for human review.
Reviewed by clawgenti using the github-pr-review skill
esnible
left a comment
There was a problem hiding this comment.
Summary
Clean shell-script refactor: scripts/program-lib.sh (~985 lines) split into four self-contained modules (core.sh, github-api.sh, fork.sh, org.sh) behind an unchanged aggregator entrypoint. Verified by hand that function bodies moved verbatim (only documented deviations: a bash 3.2 portability fix in link-health-scanner.sh and a shellcheck-comment split), load-once guards are unique per module, and only program-lib.sh sources all four (the other three have zero sibling source statements, confirming the guard-only/self-contained claim). CI's actions/checkout SHA pin was independently verified against upstream — it resolves correctly to the real v4 release. Tests (test-lib-inventory.sh, test-lib-modules.sh) match their claims (23-function inventory unchanged; each module sources standalone). Docs are consistent with the code.
Author: rubambiza (MEMBER — maintainer)
Areas reviewed: Shell, CI/GitHub Actions, Tests, Docs
Agent/IDE config (.claude/.vscode): none
Commits: 14 commits, all signed-off: yes
CI status: passing (DCO, test x2, verify-pr-title)
| # Drop any existing row for this repo, then append the new count. | ||
| ISSUE_COUNTS=$(printf '%s\n' "$ISSUE_COUNTS" | awk -F'\t' -v r="$1" '$1!=r') | ||
| ISSUE_COUNTS="${ISSUE_COUNTS} | ||
| $1 $(( cur + 1 ))" |
There was a problem hiding this comment.
nit: ISSUE_COUNTS starts as "" and each _lhs_ic_incr append prepends a newline, so the accumulator permanently carries one leading blank line/row. Harmless — _lhs_ic_get's awk match on $1==r never matches a real repo string against the empty field — but slightly untidy if this is ever dumped/debugged directly.
Summary
Decompose the ~985-line
scripts/program-lib.shshell library into four flat, self-contained modules behind an unchanged aggregator entrypoint. Pure refactor — no behavior change.core.shgithub-api.shghwrapper, issue read/close/PR-checkfork.shorg.shprogram-lib.shEach module has a load-once guard and self-sources only the dependencies it actually references (verified:
github-api.sh,fork.sh, andorg.shreference no other module, so they are guard-only; only the aggregator sources all four). The 9 consumer scripts keepsource "$SCRIPT_DIR/program-lib.sh"unchanged.Why
The monolith mixes unrelated concerns and forces wholesale copying when the scripts are vendored per-skill (the Agent Spec does not support referencing external scripts). Focused modules are easier to read, review, and vendor as a subset.
Also in this PR
link-health-scanner.shuseddeclare -A ISSUE_COUNTS(bash 4), which fails on macOS's default bash 3.2. Replaced with a newline-delimitedrepo<TAB>countaccumulator. No output change..github/workflows/tests.ymlruns the test suite onubuntu-latest+macos-latest(the macOS leg exercises BSD coreutils and the bash-3.2 floor) and shellchecks the library modules.Equivalence verification
tests/test-lib-inventory.sh): the 23-function public surface is unchanged — none lost or renamed.tests/test-lib-modules.sh): each module sources on its own with its functions defined.ORG=rossoctl, extract-broken-links, parse-diff-map).# shellcheck disable=SC2086 -- commentoncanonical_repo_for_dirinto a comment line plus a clean directive (same effect).Notes
--severity=warning. Broader consumer-script linting (a pre-existingSC2034arg-parsing idiom) is tracked as a separate follow-up (ci: Make consumer scripts shellcheck-clean and widen lint scope #52), out of scope here per scope discipline.Fixes #35
Assisted-By: Claude Code