Skip to content

refactor: Decompose program-lib.sh into portable modules (#35) - #50

Open
rubambiza wants to merge 11 commits into
rossoctl:mainfrom
rubambiza:feat/program-lib-decomposition
Open

refactor: Decompose program-lib.sh into portable modules (#35)#50
rubambiza wants to merge 11 commits into
rossoctl:mainfrom
rubambiza:feat/program-lib-decomposition

Conversation

@rubambiza

@rubambiza rubambiza commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

What

Decompose the ~985-line scripts/program-lib.sh shell library into four flat, self-contained modules behind an unchanged aggregator entrypoint. Pure refactor — no behavior change.

Module Responsibility
core.sh Workspace/temp, portable date math, JSON-schema validation, scan diffing, report I/O
github-api.sh Rate-limit-aware gh wrapper, issue read/close/PR-check
fork.sh Fork/PR creation, issue-field validation, link-fix candidate scoring
org.sh Org-profile loading, core-repo allowlist, canonical-name remap, repos-dir validation
program-lib.sh Thin aggregator sourcing the four modules — the stable entrypoint

Each module has a load-once guard and self-sources only the dependencies it actually references (verified: github-api.sh, fork.sh, and org.sh reference no other module, so they are guard-only; only the aggregator sources all four). The 9 consumer scripts keep source "$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

  • Portability fix: link-health-scanner.sh used declare -A ISSUE_COUNTS (bash 4), which fails on macOS's default bash 3.2. Replaced with a newline-delimited repo<TAB>count accumulator. No output change.
  • CI: new .github/workflows/tests.yml runs the test suite on ubuntu-latest + macos-latest (the macOS leg exercises BSD coreutils and the bash-3.2 floor) and shellchecks the library modules.
  • Docs: README section describing the module layout and the bash-3.2+ portability contract.

Equivalence verification

  • Function-inventory guard (tests/test-lib-inventory.sh): the 23-function public surface is unchanged — none lost or renamed.
  • Per-module standalone smoke test (tests/test-lib-modules.sh): each module sources on its own with its functions defined.
  • Full existing suite green (org-profile, core-repos, pr-review-impact, pr-review-integration with ORG=rossoctl, extract-broken-links, parse-diff-map).
  • Function bodies moved verbatim; the only non-verbatim change is splitting an unparseable inline # shellcheck disable=SC2086 -- comment on canonical_repo_for_dir into a comment line plus a clean directive (same effect).

Notes

Fixes #35

Assisted-By: Claude Code

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>
@rubambiza rubambiza added enhancement New feature or request ready-for-ai-review Request automated AI code review from clawgenti labels Aug 10, 2026

@clawgenti clawgenti left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread scripts/fork.sh
# ## Portability
# Targets bash 3.2+ (macOS default) through modern bash.
[ -n "${_FORK_SH_LOADED:-}" ] && return
_FORK_SH_LOADED=1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

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>
@rubambiza

Copy link
Copy Markdown
Contributor Author

Thanks — good catch on the spec/implementation drift.

Resolved by updating the design doc (commit a58ea67) rather than adding the source lines. The implementation is intentional: I verified empirically that no module (github-api.sh, fork.sh, org.sh) calls a core.sh or github-api.sh helper — every function invokes gh/git/jq/builtins directly — so all four modules are guard-only. Adding unused source lines would declare dependencies the code does not have and cut against the vendoring goal (a copied fork.sh should be genuinely self-contained).

The spec now states the load list is driven by measured call-sites, documents the self-source pattern to add only if a real cross-module call is later introduced, and notes the current split has no intra-library dependencies.

Assisted-By: Claude Code

@clawgenti clawgenti left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 "run shellcheck over scripts/*.sh" but .github/workflows/tests.yml targets 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 than scripts/*.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`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request ready-for-ai-review Request automated AI code review from clawgenti

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor: Decompose program-lib.sh into composable, portable modules

2 participants