Skip to content

fix: unit test isolation, CI feedback, and config priority#2

Merged
forkni merged 14 commits intomainfrom
development
Apr 8, 2026
Merged

fix: unit test isolation, CI feedback, and config priority#2
forkni merged 14 commits intomainfrom
development

Conversation

@forkni
Copy link
Copy Markdown
Owner

@forkni forkni commented Apr 8, 2026

Changes

  • d1262b8 test: fix all integration test failures (92/92 passing)
  • 1ee06da fix: env vars now take priority over .cgw.conf (save/restore pattern)
  • 710c442 fix: correct 5 unit test failures — grep double-output, bats stderr capture, pipe quoting, test repo root
  • 25c510e fix: address Charlie CI feedback — lint flow, blocking, shellcheck compliance
  • 79b8a94 fix: escape pipe chars in sed replacement for hook pattern generation
  • a6bda3b test: add bats-core testing pipeline for all CGW scripts
  • 2d8c4f2 chore: add internal dev files to .gitignore
  • eeb62bc feat: add PR workflow, --skip-lint flags, and ShellCheck compliance

Test Status

All 136 tests passing (44 unit + 92 integration).

Branch

developmentmain

forkni and others added 7 commits April 7, 2026 20:06
- New create_pr.sh: GitHub PR creation via gh CLI (triggers Charlie CI)
- New CGW_MERGE_MODE config: "direct" (local merge) or "pr" (GitHub PR)
- Add --skip-lint / --skip-md-lint to commit_enhanced.sh, check_lint.sh, push_validated.sh
- Add CGW_MARKDOWNLINT_CMD support for dedicated markdown linting
- Fix ShellCheck warnings: export CGW_ALL_PREFIXES, disable SC2034 for reserved params
- Fix SC2126 in merge_docs.sh: grep -vc instead of grep | wc -l
- Update auto-git-workflow command with Phase 4A/4B (direct vs PR merge)
- Update README with branch setup, PR workflow, CI section
- Update cgw.conf.example with CGW_MERGE_MODE docs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CLAUDE.md, SESSION_LOG.md, .claude/commands/, .claude/skills/
are local-only development artifacts and should not be committed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two test tiers covering 13 shell scripts:
- Unit tests (tests/unit/): pure function tests for _common.sh and _config.sh
- Integration tests (tests/integration/): full script tests against temp git repos

Helpers: tests/helpers/setup.bash (temp repo creation) and mocks.bash (mock lint/gh CLI).
CI: add test job to branch-protection.yml (bats-core installed via git clone).
gitattributes: add *.bats and *.bash → LF enforcement.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@charliecreates charliecreates Bot requested a review from CharlieHelps April 8, 2026 01:59
Copy link
Copy Markdown

@charliecreates charliecreates Bot left a comment

Choose a reason for hiding this comment

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

check_lint.sh has a real regression: formatting checks are now effectively disabled unless CGW_LINT_CMD is set, and the “no tools configured” fast-path ignores CGW_FORMAT_CMD. The .cgw.conf env-priority implementation can still yield inconsistent derived values because env vars are restored only after the config is evaluated. create_pr.sh masks branch-compare errors as “0 commits ahead” and should validate origin/${CGW_TARGET_BRANCH} (and ideally local-vs-remote sync) to avoid misleading failures. Test helpers have at least one portability issue (mktemp) and one integration test that doesn’t hit its intended code path.

Additional notes (4)
  • Maintainability | scripts/git/check_lint.sh:63-63
    check_lint.sh now treats “no lint tool configured” as “no checks configured”, but it ignores CGW_FORMAT_CMD. This means a repo that disables code lint (CGW_LINT_CMD="") but still wants formatting enforcement (CGW_FORMAT_CMD set) will incorrectly skip all checks and exit 0.

Additionally, the format check is currently nested under if [[ -n "${CGW_LINT_CMD}" ]], so format will never run unless a lint command is also configured—this is a functional regression from the prior behavior.

  • Maintainability | scripts/git/push_validated.sh:177-185
    push_validated.sh decides whether to run the pre-push lint step by checking only CGW_LINT_CMD and CGW_MARKDOWNLINT_CMD. This again ignores CGW_FORMAT_CMD, so “format-only” setups won’t be validated on push even though check_lint.sh is intended to cover formatting too.

Also, since check_lint.sh already knows how to self-skip, the extra gating here is easy to get out of sync (as happened with CGW_FORMAT_CMD).

  • Maintainability | tests/integration/create_pr.bats:99-114
    This test claims to cover the “no commits ahead of target” case, but it sets CGW_SOURCE_BRANCH=main and CGW_TARGET_BRANCH=main, which will always hit the earlier “source == target” validation and never exercise the “ahead count is 0” path. That leaves the intended branch-comparison behavior untested.

  • Compatibility | scripts/git/configure.sh:239-246
    The sed replacement for __CGW_LOCAL_FILES_PATTERN__ is still not safe/correct for patterns containing backslashes (and &).

You’re building regex fragments like CLAUDE\.md (good), but when interpolated into sed "s|...|${...}|", sed treats backslashes in the replacement specially and can consume them, resulting in the generated hook containing CLAUDE.md (dot becomes wildcard) rather than CLAUDE\.md.

Escaping only the | delimiter is not sufficient here.

Summary of changes

Overview

This PR significantly expands CGW’s automation and quality gates:

  • Config precedence: updates scripts/git/_config.sh so CGW_* environment variables are restored after sourcing .cgw.conf (intended: env > config > defaults).
  • Lint flow updates: adds --skip-lint / --skip-md-lint support and introduces an optional dedicated markdown lint step via CGW_MARKDOWNLINT_CMD across:
    • scripts/git/check_lint.sh
    • scripts/git/commit_enhanced.sh
    • scripts/git/push_validated.sh
  • New PR workflow: adds scripts/git/create_pr.sh to open GitHub PRs via gh (supports --draft, --dry-run, non-interactive mode) and documents PR-based promotion via CGW_MERGE_MODE.
  • Docs + examples: updates README.md, command/auto-git-workflow.md, and cgw.conf.example to describe branch setup, CI, PR mode, and new flags.
  • CI + tests: adds a GitHub Actions test job and introduces a comprehensive bats-core test suite (unit + integration), plus test fixtures/helpers.
  • Misc: enforces LF endings for *.bats/*.bash, expands .gitignore for local dev artifacts, and includes minor ShellCheck-driven cleanups (e.g., merge_docs.sh, _common.sh).

Comment thread scripts/git/_config.sh Outdated
Comment on lines +49 to +59
if [[ -f "${_CGW_CONF}" ]]; then
# Save CGW_* env vars so they take priority over .cgw.conf
_cgw_saved_env=$(env | grep "^CGW_" || true)
# shellcheck source=/dev/null
source "${_CGW_CONF}"
# Restore: env vars take priority over .cgw.conf values
while IFS= read -r _line; do
# shellcheck disable=SC2163 # export VAR=VALUE form: _line contains "KEY=VALUE" literal string
[[ -n "${_line}" ]] && export "${_line?}"
done <<< "${_cgw_saved_env}"
unset _cgw_saved_env _line
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The .cgw.conf “env wins” implementation restores CGW_* variables after sourcing the config. That does make final values come from the environment, but it can still produce internally inconsistent config when .cgw.conf computes one variable from another.

Example failure mode:

  • Env sets CGW_TARGET_BRANCH=release
  • .cgw.conf sets CGW_TARGET_BRANCH=main and then sets CGW_PROTECTED_BRANCHES="${CGW_TARGET_BRANCH} staging"
  • After restore, CGW_TARGET_BRANCH becomes release, but CGW_PROTECTED_BRANCHES remains main staging

This contradicts the stated “three-tier resolution” behavior (env > config > defaults) in practice for derived values.

Suggestion

Instead of sourcing .cgw.conf verbatim and restoring afterward, load it in a way that skips assignments to variables that were already set in the environment, so env values remain visible throughout config evaluation.

One pragmatic approach (given .cgw.conf is generated and should mostly be assignments/comments) is:

# record exported CGW_* keys present before reading config
declare -A _cgw_env_keys=()
while IFS= read -r k; do _cgw_env_keys["${k}"]=1; done < <(compgen -e | grep '^CGW_' || true)

if [[ -f "${_CGW_CONF}" ]]; then
  while IFS= read -r line; do
    # pass through blank lines/comments
    [[ -z "${line}" || "${line}" =~ ^[[:space:]]*# ]] && continue

    if [[ "${line}" =~ ^[[:space:]]*(CGW_[A-Z0-9_]+)= ]]; then
      key="${BASH_REMATCH[1]}"
      [[ -n "${_cgw_env_keys[${key}]+x}" ]] && continue
    fi

    eval "${line}"
  done < "${_CGW_CONF}"
fi
unset _cgw_env_keys key line

Reply with "@CharlieHelps yes please" if you want me to put together a commit (and adjust/extend tests) for this safer precedence behavior.

Comment thread scripts/git/create_pr.sh Outdated
Comment on lines +132 to +155
# Verify source branch exists locally and remotely
if ! git show-ref --verify "refs/heads/${CGW_SOURCE_BRANCH}" > /dev/null 2>&1; then
err "Source branch '${CGW_SOURCE_BRANCH}' does not exist locally"
echo " Create it with: git checkout -b ${CGW_SOURCE_BRANCH}" >&2
log_section_end "BRANCH VALIDATION" "$logfile" "1"
exit 1
fi

if ! git ls-remote --exit-code origin "refs/heads/${CGW_SOURCE_BRANCH}" > /dev/null 2>&1; then
err "Source branch '${CGW_SOURCE_BRANCH}' not pushed to origin"
echo " Push it with: ./scripts/git/push_validated.sh" >&2
log_section_end "BRANCH VALIDATION" "$logfile" "1"
exit 1
fi

# Check for commits ahead of target
local commits_ahead
commits_ahead=$(git rev-list --count "origin/${CGW_TARGET_BRANCH}..origin/${CGW_SOURCE_BRANCH}" 2>/dev/null || echo "0")

if [[ "${commits_ahead}" == "0" ]]; then
echo "[!] No commits ahead of ${CGW_TARGET_BRANCH} — nothing to PR" | tee -a "$logfile"
log_section_end "BRANCH VALIDATION" "$logfile" "1"
exit 1
fi
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

create_pr.sh doesn’t verify that origin/${CGW_TARGET_BRANCH} exists and it masks git rev-list failures by coercing errors to 0. In those cases, the script will report “nothing to PR” even when the real problem is “target branch missing/unfetched” or another comparison error.

It also doesn’t check that the local source branch is actually in sync with origin/${CGW_SOURCE_BRANCH} (a common cause of surprise when the PR doesn’t include local commits).

Suggestion

Tighten branch validation and don’t coerce comparison errors into “0 commits”. For example:

# verify target exists remotely too
if ! git ls-remote --exit-code origin "refs/heads/${CGW_TARGET_BRANCH}" >/dev/null 2>&1; then
  err "Target branch '${CGW_TARGET_BRANCH}' not found on origin"
  log_section_end "BRANCH VALIDATION" "$logfile" "1"
  exit 1
fi

# ensure local HEAD matches origin (optional but very useful)
if [[ "$(git rev-parse "${CGW_SOURCE_BRANCH}")" != "$(git rev-parse "origin/${CGW_SOURCE_BRANCH}")" ]]; then
  err "Local '${CGW_SOURCE_BRANCH}' is not pushed to origin (run push_validated.sh)"
  log_section_end "BRANCH VALIDATION" "$logfile" "1"
  exit 1
fi

# fail loudly if rev-list cannot compare
if ! commits_ahead=$(git rev-list --count "origin/${CGW_TARGET_BRANCH}..origin/${CGW_SOURCE_BRANCH}" 2>/dev/null); then
  err "Unable to compare origin/${CGW_TARGET_BRANCH}..origin/${CGW_SOURCE_BRANCH} (fetch? missing branch?)"
  log_section_end "BRANCH VALIDATION" "$logfile" "1"
  exit 1
fi

Reply with "@CharlieHelps yes please" if you’d like me to add a commit implementing these validation improvements.

Comment thread tests/helpers/setup.bash
Comment on lines +11 to +15
# create_temp_dir — set BATS_TEST_TMPDIR to an isolated temp dir
create_temp_dir() {
TEST_TMPDIR="$(mktemp -d)"
export TEST_TMPDIR
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

mktemp -d without a template is not portable across macOS/BSD mktemp (commonly errors with “too few X’s in template”). Since the repo targets macOS as well, the test helper should be portable to avoid local test runs failing.

Suggestion

Make temp dir creation portable across GNU/BSD mktemp implementations:

create_temp_dir() {
  TEST_TMPDIR="$(mktemp -d 2>/dev/null || mktemp -d -t cgw.XXXXXX)"
  export TEST_TMPDIR
}

Reply with "@CharlieHelps yes please" if you want me to add a commit updating the helper accordingly.

Comment thread tests/helpers/mocks.bash
Comment on lines +94 to +100
# hide_gh
# Removes `gh` from PATH by creating a directory-of-blocked-cmds
# that the shell searches before real PATH entries. `gh` is absent → command -v fails.
hide_gh() {
# setup_mock_bin already prepended MOCK_BIN_DIR; just ensure no gh there
rm -f "${MOCK_BIN_DIR}/gh"
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

hide_gh() doesn’t actually “hide” the system gh binary. It only removes ${MOCK_BIN_DIR}/gh, but PATH still contains the runner’s real gh, so command -v gh will succeed on environments where gh is preinstalled (notably GitHub-hosted runners). This makes the “no gh CLI in PATH” test non-deterministic and likely to fail in CI.

Suggestion

Make hide_gh() deterministic by running the script under a PATH that excludes the real gh while still allowing git.

One pragmatic approach for tests:

  1. Add a helper to install a git shim into ${MOCK_BIN_DIR}:
install_git_shim() {
  local real_git
  real_git="$(command -v git)"
  cat > "${MOCK_BIN_DIR}/git" <<EOF
#!/usr/bin/env bash
exec "${real_git}" "\$@"
EOF
  chmod +x "${MOCK_BIN_DIR}/git"
}
  1. Then in hide_gh(), set:
install_git_shim
export PATH="${MOCK_BIN_DIR}:/bin"
rm -f "${MOCK_BIN_DIR}/gh"

Reply with "@CharlieHelps yes please" if you want me to add a commit that implements this (and adjusts any tests that rely on the current PATH behavior).

Comment on lines +88 to +94
- name: Install bats-core and helpers
run: |
git clone --depth 1 https://github.com/bats-core/bats-core.git /tmp/bats
sudo /tmp/bats/install.sh /usr/local
git clone --depth 1 https://github.com/bats-core/bats-support.git /tmp/bats-support
git clone --depth 1 https://github.com/bats-core/bats-assert.git /tmp/bats-assert

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The workflow installs bats via git clone --depth 1 from default branches. That’s brittle (upstream changes can break CI unexpectedly) and increases supply-chain risk compared to pinning versions.

This is especially relevant since branch protection will depend on these checks.

Suggestion

Pin bats-core/bats-support/bats-assert to tags or commit SHAs, or switch to a maintained action (e.g. bats-core/bats-action) with version pinning.

Reply with "@CharlieHelps yes please" if you want me to add a commit that pins these dependencies (either via tags/SHAs or an action).

@charliecreates charliecreates Bot removed the request for review from CharlieHelps April 8, 2026 02:06
forkni and others added 2 commits April 7, 2026 22:55
Scripts:
- check_lint.sh: Fix early-exit condition to include CGW_FORMAT_CMD; un-nest
  format check from lint check block so it runs independently (regression fix)
- push_validated.sh: Add CGW_FORMAT_CMD to pre-push lint gate
- _config.sh: Replace source+restore pattern with line-by-line parsing that
  skips vars already in env, fixing derived-value inconsistency; allow
  PROJECT_ROOT env var override to skip auto-detection
- create_pr.sh: Add target branch remote validation; replace error-masking
  '|| echo 0' with proper exit-code check; add local/remote sync warning
- configure.sh: Escape backslashes in sed replacement before escaping pipes

Tests:
- create_pr.bats: Fix 'no commits ahead' test to actually exercise that code
  path; fix _run_create_pr helper (PATH single-quote bug, stderr capture,
  empty-arg passthrough, PROJECT_ROOT isolation); fix inline bash -c blocks
- mocks.bash: hide_gh() installs a shim that shadows system gh
- setup.bash: Portable mktemp fallback for macOS/BSD

CI:
- branch-protection.yml: Pin bats installs to v1.13.0/v0.3.0/v2.2.4

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fix five categories of pre-existing test isolation bugs:

- configure.sh: guard PROJECT_ROOT detection with env var check so tests
  can pin the root to their temp repo (was always overwriting with real CGW root)
- commit_enhanced.sh: treat --unknown-flag args as errors (exit 1) instead
  of silently capturing them as the commit message
- validate_branches.sh: exit non-zero when CGW_SOURCE_BRANCH does not
  exist in the repo (was only warning about branch relationship)
- merge_validation.bats: checkout development before calling _run_merge;
  merge_with_validation.sh refuses to run from the target branch
- All integration _run_* helpers: remove bad export PATH= lines and add
  export PROJECT_ROOT= so scripts target the test repo, not the real project

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@forkni
Copy link
Copy Markdown
Owner Author

forkni commented Apr 8, 2026

@CharlieHelps review latest commits

Copy link
Copy Markdown

@charliecreates charliecreates Bot left a comment

Choose a reason for hiding this comment

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

  • scripts/git/_config.sh’s .cgw.conf loader is still too permissive/fragile: it can crash on non-assignment lines and evals anything not filtered, so it should be restricted to supported CGW_* assignments.
  • scripts/git/create_pr.sh relies on origin/<branch> refs without ensuring they exist/are current; adding a targeted git fetch would prevent avoidable comparison failures.
  • scripts/git/configure.sh’s sed replacement escaping still misses &, which can corrupt generated hook patterns.
  • tests/helpers/mocks.bash hide_gh() doesn’t actually test the “gh missing” branch because command -v gh will still succeed.
Additional notes (1)
  • Maintainability | README.md:110-117
    This documentation recommends a raw git push during branch setup. That conflicts with Git Workflow Rules [R1] (“Always use CGW wrapper scripts — never run raw git push … directly”). If the intent is that initial branch setup is an exception, it should be stated; otherwise, readers will bypass safety checks by following the README.
Summary of changes

Summary

This PR expands CGW’s automation, configuration, CI, and test coverage.

CI / repo config

  • Enforced LF endings for *.bats / *.bash via .gitattributes.
  • Added a test job to .github/workflows/branch-protection.yml running unit + integration Bats suites (with version-pinned Bats deps).
  • Ignored additional local-only dev artifacts in .gitignore (CLAUDE.md, SESSION_LOG.md, .claude/...).

Config & workflow

  • Updated scripts/git/_config.sh to:
    • Allow PROJECT_ROOT to be pinned via env (test/CI isolation).
    • Load .cgw.conf line-by-line so env vars take priority during evaluation.
    • Introduce CGW_MARKDOWNLINT_CMD / CGW_MARKDOWNLINT_ARGS and CGW_MERGE_MODE.
  • Improved hook generation in configure.sh by escaping backslashes before sed substitution.

Lint/format gates

  • Extended check_lint.sh, commit_enhanced.sh, and push_validated.sh with --skip-lint / --skip-md-lint (and env equivalents).
  • Fixed check_lint.sh to run format checks independently of code lint.

New functionality

  • Added scripts/git/create_pr.sh to open PRs via gh (supports --draft, --dry-run, --non-interactive) and added docs for PR-based promotion.

Tests

  • Added comprehensive Bats unit and integration test suites plus helpers/mocks and fixtures.

Comment thread scripts/git/_config.sh Outdated
Comment on lines +53 to +68
# Read .cgw.conf line-by-line, only applying variables not already set in the environment.
# This ensures env vars take priority AND derived values (e.g. CGW_PROTECTED_BRANCHES
# referencing CGW_TARGET_BRANCH) stay consistent with the values actually used.
while IFS= read -r _line; do
[[ "${_line}" =~ ^[[:space:]]*# ]] && continue # skip comments
[[ "${_line}" =~ ^[[:space:]]*$ ]] && continue # skip blank lines
# Strip leading 'export ' if present, isolate the variable name
_cgw_var="${_line#export }"
_cgw_var="${_cgw_var%%=*}"
_cgw_var="${_cgw_var## }"
# Only set if not already in environment (preserves env var priority)
if [[ -z "${!_cgw_var+x}" ]]; then
# shellcheck disable=SC2163 # eval required: _line contains "KEY=VALUE" or "export KEY=VALUE"
eval "${_line}"
fi
done < "${_CGW_CONF}"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

_config.sh now avoids a full source of .cgw.conf, but it still evals any non-comment/non-blank line and also assumes the left-hand side can be safely turned into a variable name.

That’s fragile:

  • A malformed line (or even a legitimate non-assignment like unset CGW_FOO) will produce an invalid _cgw_var and can hard-fail due to indirect expansion (${!_cgw_var+x}) under set -u.
  • It also expands the blast radius of .cgw.conf beyond “assignments only” (even though the comment implies it’s assignment-focused).

Given .cgw.conf is user-editable, this should be hardened to only evaluate assignment lines you explicitly support (preferably only CGW_* keys).

Suggestion

Tighten parsing to only accept CGW_...=... (optionally prefixed by export) and derive the key via regex capture instead of string slicing. This avoids invalid indirect expansions and avoids executing unexpected shell statements.

Example:

if [[ -f "${_CGW_CONF}" ]]; then
  while IFS= read -r _line; do
    [[ "${_line}" =~ ^[[:space:]]*# ]] && continue
    [[ "${_line}" =~ ^[[:space:]]*$ ]] && continue

    if [[ "${_line}" =~ ^[[:space:]]*(export[[:space:]]+)?(CGW_[A-Z0-9_]+)= ]]; then
      _cgw_var="${BASH_REMATCH[2]}"
      if [[ -z "${!_cgw_var+x}" ]]; then
        # shellcheck disable=SC2163  # eval required: config lines are assignments
        eval "${_line}"
      fi
    else
      err "Ignoring unsupported line in .cgw.conf: ${_line}"
    fi
  done < "${_CGW_CONF}"
  unset _line _cgw_var
fi

Reply with "@CharlieHelps yes please" if you’d like me to add a commit implementing this hardened parser and adjusting tests accordingly.

Comment thread scripts/git/configure.sh Outdated
Comment on lines +244 to +248
# Escape backslashes first (e.g. \. from dot-escaping), then | (sed delimiter)
local sed_files_pattern="${files_pattern//\\/\\\\}"
sed_files_pattern="${sed_files_pattern//|/\\|}"
mkdir -p "${PROJECT_ROOT}/.githooks"
sed "s|__CGW_LOCAL_FILES_PATTERN__|${files_pattern}|g" \
sed "s|__CGW_LOCAL_FILES_PATTERN__|${sed_files_pattern}|g" \
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

sed replacement escaping improved by handling backslashes and the | delimiter, but it still doesn’t escape &, which sed treats specially in the replacement (it expands to the entire match).

If a user adds a local-only file containing & to CGW_LOCAL_FILES, the generated hook pattern can be corrupted in a way that’s hard to debug.

Suggestion

Escape & for the sed replacement string in addition to backslashes and delimiter characters.

Example:

local sed_files_pattern="${files_pattern//\\/\\\\}"
sed_files_pattern="${sed_files_pattern//&/\\&}"
sed_files_pattern="${sed_files_pattern//|/\\|}"

Reply with "@CharlieHelps yes please" if you want me to add a small commit covering this sed replacement edge case.

@forkni
Copy link
Copy Markdown
Owner Author

forkni commented Apr 8, 2026

Addressed Charlie CI Round-2 Feedback

All 5 review items resolved in commit b7a525a:

# Issue Fix
1 _config.sh loader evals arbitrary lines Regex-gated to CGW_[A-Z0-9_]+= only; warns on unrecognized lines
2 create_pr.sh stale origin/ refs Added git fetch origin <source> <target> before comparisons
3 configure.sh sed missing & escaping Added &\& escaping in replacement string
4 hide_gh() doesn't fool command -v Replaced shim with PATH filtering so command -v gh properly fails
5 README raw git push conflicts with R1 Added note documenting one-time bootstrap exception

Bonus: Added .shellcheckrc so local shellcheck runs match CI flags (-x --source-path=scripts/git).

@forkni
Copy link
Copy Markdown
Owner Author

forkni commented Apr 8, 2026

@CharlieHelps review latest commits

Copy link
Copy Markdown

@charliecreates charliecreates Bot left a comment

Choose a reason for hiding this comment

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

The .cgw.conf parser in _config.sh has broken comment/blank-line skip regexes, which will generate noisy “unsupported line” warnings for normal configs and should be fixed. create_pr.sh still has avoidable reliability and policy issues: it silences git fetch failures (risking stale comparisons) and prints guidance that violates Git Workflow Rules [R1] by recommending raw git push. push_validated.sh doesn’t honor CGW_SKIP_LINT, creating inconsistent env-driven behavior across scripts. commit_enhanced.sh introduces ShellCheck suppressions without the required [R6] justifications.

Additional notes (1)
  • Maintainability | scripts/git/push_validated.sh:71-71
    push_validated.sh honors CGW_SKIP_MD_LINT, but it does not honor CGW_SKIP_LINT even though check_lint.sh and commit_enhanced.sh do. This inconsistency makes automation harder (env-based skipping works for commit/check, but not push).
Summary of changes

Overview

This PR expands CGW’s workflow automation, CI coverage, and configuration behavior.

Key changes

  • Repo / CI

    • Enforce LF endings for *.bats / *.bash via .gitattributes.
    • Add a test job to .github/workflows/branch-protection.yml running Bats unit + integration suites (version-pinned deps).
    • Add .shellcheckrc to align local shellcheck runs with CI.
    • Extend .gitignore with local dev artifacts (e.g. CLAUDE.md, .claude/...).
  • Configuration + precedence

    • Allow PROJECT_ROOT to be provided via env to support test isolation.
    • Change .cgw.conf loading to a line-by-line parser that preserves env-var priority during evaluation.
    • Add config for CGW_MARKDOWNLINT_CMD / CGW_MARKDOWNLINT_ARGS and CGW_MERGE_MODE.
  • Workflow scripts

    • check_lint.sh: add --skip-lint / --skip-md-lint, independent format checks, and optional markdown lint step.
    • commit_enhanced.sh: add --skip-lint / --skip-md-lint and markdown lint integration.
    • push_validated.sh: pass through markdown-lint skipping to check_lint.sh and include format/md-lint tools in gating.
    • configure.sh: harden sed replacement escaping (handle \, &, and |).
    • New scripts/git/create_pr.sh to open PRs via gh (supports --draft, --dry-run, --non-interactive).
  • Tests

    • Add Bats helpers (tests/helpers/setup.bash, tests/helpers/mocks.bash), a config fixture, and new unit/integration suites covering core scripts.

Docs

  • Update README.md, command/auto-git-workflow.md, and cgw.conf.example to document PR workflow, branch setup, CI, and new config flags.

Comment thread scripts/git/create_pr.sh Outdated
Comment on lines +256 to +268
local pr_url
if pr_url=$(gh pr create "${gh_flags[@]}" 2>&1 | tee -a "$logfile"); then
log_section_end "CREATE PR" "$logfile" "0"
echo "" | tee -a "$logfile"
echo "✓ PR created: ${pr_url}" | tee -a "$logfile"
echo "" | tee -a "$logfile"
if [[ ${draft} -eq 0 ]]; then
echo "Charlie CI will auto-review this PR." | tee -a "$logfile"
else
echo "Draft PR created. Mark as Ready for Review to trigger Charlie CI." | tee -a "$logfile"
fi
echo "Full log: $logfile"
else
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

pr_url currently captures all gh pr create output (including any non-URL text), and then you print it as PR created: ${pr_url}. If gh emits multi-line output, this will produce messy logs and potentially confuse callers that expect a single URL.

Suggestion

Prefer making gh return a single URL and log the full output separately. For example:

local pr_url
if pr_url=$(gh pr create "${gh_flags[@]}" --json url -q .url 2>&1 | tee -a "${logfile}"); then
  echo "PR created: ${pr_url}" | tee -a "${logfile}"
else
  ...
fi

Reply with "@CharlieHelps yes please" if you’d like me to add a commit implementing this and adjusting the integration test assertions accordingly.

Comment thread scripts/git/commit_enhanced.sh Outdated
Comment on lines +259 to +316

# shellcheck disable=SC2086
lint_output=$("${lint_cmd}" ${CGW_LINT_CHECK_ARGS} ${CGW_LINT_EXCLUDES} 2>&1) || python_lint_error=1
if [[ -n "$lint_output" ]] && [[ "$lint_output" != *"All checks passed"* ]]; then
echo "[LINT ERRORS]" | tee -a "$logfile"
echo "$lint_output" | tee -a "$logfile"
fi
local lint_output format_output

if [[ -n "${CGW_FORMAT_CMD}" ]]; then
# shellcheck disable=SC2086
format_output=$("${CGW_FORMAT_CMD}" ${CGW_FORMAT_CHECK_ARGS} ${CGW_FORMAT_EXCLUDES} 2>&1) || python_lint_error=1
if [[ -n "$format_output" ]] && [[ "$format_output" == *"would reformat"* ]]; then
echo "[FORMAT ERRORS]" | tee -a "$logfile"
echo "$format_output" | tee -a "$logfile"
lint_output=$("${lint_cmd}" ${CGW_LINT_CHECK_ARGS} ${CGW_LINT_EXCLUDES} 2>&1) || python_lint_error=1
if [[ -n "$lint_output" ]] && [[ "$lint_output" != *"All checks passed"* ]]; then
echo "[LINT ERRORS]" | tee -a "$logfile"
echo "$lint_output" | tee -a "$logfile"
fi
fi

log_section_end "LINT CHECK" "$logfile" "$python_lint_error"

if [[ ${python_lint_error} -eq 1 ]]; then
echo "[!] Lint errors detected"
if [[ ${non_interactive} -eq 1 ]]; then
echo "[Non-interactive] Auto-fixing lint issues..."
if [[ -n "${CGW_FORMAT_CMD}" ]]; then
# shellcheck disable=SC2086
"${lint_cmd}" ${CGW_LINT_FIX_ARGS} ${CGW_LINT_EXCLUDES} 2>&1 | tee -a "$logfile"
if [[ -n "${CGW_FORMAT_CMD}" ]]; then
# shellcheck disable=SC2086
"${CGW_FORMAT_CMD}" ${CGW_FORMAT_FIX_ARGS} ${CGW_FORMAT_EXCLUDES} 2>&1 | tee -a "$logfile"
fi

if [[ ${staged_only} -eq 0 ]]; then
git add .
unstage_local_only_files
format_output=$("${CGW_FORMAT_CMD}" ${CGW_FORMAT_CHECK_ARGS} ${CGW_FORMAT_EXCLUDES} 2>&1) || python_lint_error=1
if [[ -n "$format_output" ]] && [[ "$format_output" == *"would reformat"* ]]; then
echo "[FORMAT ERRORS]" | tee -a "$logfile"
echo "$format_output" | tee -a "$logfile"
fi
fi

# Re-check
python_lint_error=0
# shellcheck disable=SC2086
"${lint_cmd}" ${CGW_LINT_CHECK_ARGS} ${CGW_LINT_EXCLUDES} 2>&1 | tee -a "$logfile" || python_lint_error=1
log_section_end "LINT CHECK" "$logfile" "$python_lint_error"

if [[ ${python_lint_error} -eq 1 ]]; then
err "Lint errors remain after auto-fix"
exit 1
fi
else
read -rp "Auto-fix lint issues? (yes/no/skip): " fix_lint
case "$fix_lint" in
yes|y)
if [[ ${python_lint_error} -eq 1 ]]; then
echo "[!] Lint errors detected"
if [[ ${non_interactive} -eq 1 ]]; then
echo "[Non-interactive] Auto-fixing lint issues..."
# shellcheck disable=SC2086
"${lint_cmd}" ${CGW_LINT_FIX_ARGS} ${CGW_LINT_EXCLUDES} 2>&1 | tee -a "$logfile"
if [[ -n "${CGW_FORMAT_CMD}" ]]; then
# shellcheck disable=SC2086
"${lint_cmd}" ${CGW_LINT_FIX_ARGS} ${CGW_LINT_EXCLUDES}
if [[ -n "${CGW_FORMAT_CMD}" ]]; then
# shellcheck disable=SC2086
"${CGW_FORMAT_CMD}" ${CGW_FORMAT_FIX_ARGS} ${CGW_FORMAT_EXCLUDES}
fi
"${CGW_FORMAT_CMD}" ${CGW_FORMAT_FIX_ARGS} ${CGW_FORMAT_EXCLUDES} 2>&1 | tee -a "$logfile"
fi

if [[ ${staged_only} -eq 0 ]]; then
git add .
unstage_local_only_files
;;
skip|s)
echo "[!] Proceeding with lint warnings (CI may flag these)" ;;
*)
echo "Commit cancelled — fix lint errors first"
exit 1 ;;
esac
fi

# Re-check
python_lint_error=0
# shellcheck disable=SC2086
"${lint_cmd}" ${CGW_LINT_CHECK_ARGS} ${CGW_LINT_EXCLUDES} 2>&1 | tee -a "$logfile" || python_lint_error=1

if [[ ${python_lint_error} -eq 1 ]]; then
err "Lint errors remain after auto-fix"
exit 1
fi
else
read -rp "Auto-fix lint issues? (yes/no/skip): " fix_lint
case "$fix_lint" in
yes|y)
# shellcheck disable=SC2086
"${lint_cmd}" ${CGW_LINT_FIX_ARGS} ${CGW_LINT_EXCLUDES}
if [[ -n "${CGW_FORMAT_CMD}" ]]; then
# shellcheck disable=SC2086
"${CGW_FORMAT_CMD}" ${CGW_FORMAT_FIX_ARGS} ${CGW_FORMAT_EXCLUDES}
fi
git add .
unstage_local_only_files
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Several # shellcheck disable=SC2086 suppressions in the new/modified commit_enhanced.sh blocks are missing the required one-line justification comment.

This violates Shell Script Code Style [R6] (suppressions must include a justification).

Suggestion

Add justification comments everywhere SC2086 is suppressed, e.g.:

# shellcheck disable=SC2086  # Word splitting intentional: CGW_*_ARGS contains multiple flags
lint_output=$("${lint_cmd}" ${CGW_LINT_CHECK_ARGS} ${CGW_LINT_EXCLUDES} 2>&1) || python_lint_error=1

Reply with "@CharlieHelps yes please" if you want me to add a commit that updates the suppressions consistently across the modified blocks.

@forkni
Copy link
Copy Markdown
Owner Author

forkni commented Apr 8, 2026

Addressed Charlie CI Round-3 Feedback

All 6 review items resolved in commit d044fdf:

# Issue Fix
1 _config.sh comment/blank-line regexes had extra trailing space inside pattern Removed extra space — # ]]# ]], $ ]]$ ]]
2 create_pr.sh fetch silenced all failures with || true Now warns user if fetch fails instead of silently continuing with stale refs
3 create_pr.sh line 150 suggested raw git push origin HEAD:... (violates R1) Changed to ./scripts/git/push_validated.sh --branch <target>
4 create_pr.sh pr_url captured all gh output including stderr Now captures full output to log, extracts URL via grep separately
5 push_validated.sh didn't honor CGW_SKIP_LINT env var Added [[ "${CGW_SKIP_LINT:-0}" == "1" ]] && skip_lint=1 alongside existing CGW_SKIP_MD_LINT
6 commit_enhanced.sh 7 of 8 SC2086 suppressions lacked R6 justifications Added # Word splitting intentional: CGW_*_ARGS/CGW_*_EXCLUDES contain multiple flags to all 7

@forkni
Copy link
Copy Markdown
Owner Author

forkni commented Apr 8, 2026

@CharlieHelps review latest commits

Copy link
Copy Markdown

@charliecreates charliecreates Bot left a comment

Choose a reason for hiding this comment

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

The main functional issue is that commit_enhanced.sh still ties formatting checks to CGW_LINT_CMD, breaking “format-only” configurations and making behavior inconsistent with check_lint.sh/push_validated.sh. _config.sh now depends on err, which makes the config loader less self-contained than it should be. The test helper hide_gh() mutates PATH in a way that can remove unrelated system tools, risking environment-dependent failures. create_pr.sh’s URL extraction via regex is brittle compared to gh’s --json url output.

Summary of changes

Summary

This PR expands CGW’s workflow automation, CI, configuration precedence, and test coverage.

CI / repo hygiene

  • Enforced LF endings for *.bats/*.bash via .gitattributes.
  • Added a Bats test job to .github/workflows/branch-protection.yml (pinned tags) and introduced .shellcheckrc for local parity.
  • Extended .gitignore with local-only dev artifacts.

Config & precedence

  • scripts/git/_config.sh now supports PROJECT_ROOT overrides and loads .cgw.conf line-by-line so env vars take priority during evaluation.
  • Added config variables for CGW_MARKDOWNLINT_CMD/CGW_MARKDOWNLINT_ARGS and CGW_MERGE_MODE.

Workflow scripts

  • check_lint.sh, commit_enhanced.sh, and push_validated.sh gained --skip-lint / --skip-md-lint and optional Markdown lint support.
  • configure.sh hook generation now escapes \, &, and | for sed replacement safety.
  • Added new scripts/git/create_pr.sh to open PRs via gh with branch validation + git fetch.

Tests

  • Added unit + integration Bats suites plus shared helpers (tests/helpers/setup.bash, tests/helpers/mocks.bash) and a .cgw.conf fixture.

Comment thread scripts/git/commit_enhanced.sh Outdated
Comment on lines +246 to +289
if [[ -z "${CGW_LINT_CMD}" ]]; then
echo " (lint check skipped — CGW_LINT_CMD not set)"
else
log_section_start "LINT CHECK" "$logfile"

# Determine lint binary (venv or PATH)
local lint_cmd="${CGW_LINT_CMD}"
if [[ "${CGW_LINT_CMD}" == "ruff" ]]; then
get_python_path 2>/dev/null || true
if [[ -n "${PYTHON_BIN:-}" ]] && [[ -f "${PYTHON_BIN}/ruff${PYTHON_EXT:-}" ]]; then
lint_cmd="${PYTHON_BIN}/ruff${PYTHON_EXT:-}"
fi
fi
fi

local lint_output format_output
local lint_output format_output

# shellcheck disable=SC2086
lint_output=$("${lint_cmd}" ${CGW_LINT_CHECK_ARGS} ${CGW_LINT_EXCLUDES} 2>&1) || python_lint_error=1
if [[ -n "$lint_output" ]] && [[ "$lint_output" != *"All checks passed"* ]]; then
echo "[LINT ERRORS]" | tee -a "$logfile"
echo "$lint_output" | tee -a "$logfile"
fi

if [[ -n "${CGW_FORMAT_CMD}" ]]; then
# shellcheck disable=SC2086
format_output=$("${CGW_FORMAT_CMD}" ${CGW_FORMAT_CHECK_ARGS} ${CGW_FORMAT_EXCLUDES} 2>&1) || python_lint_error=1
if [[ -n "$format_output" ]] && [[ "$format_output" == *"would reformat"* ]]; then
echo "[FORMAT ERRORS]" | tee -a "$logfile"
echo "$format_output" | tee -a "$logfile"
# shellcheck disable=SC2086 # Word splitting intentional: CGW_LINT_CHECK_ARGS/CGW_LINT_EXCLUDES contain multiple flags
lint_output=$("${lint_cmd}" ${CGW_LINT_CHECK_ARGS} ${CGW_LINT_EXCLUDES} 2>&1) || python_lint_error=1
if [[ -n "$lint_output" ]] && [[ "$lint_output" != *"All checks passed"* ]]; then
echo "[LINT ERRORS]" | tee -a "$logfile"
echo "$lint_output" | tee -a "$logfile"
fi
fi

log_section_end "LINT CHECK" "$logfile" "$python_lint_error"

if [[ ${python_lint_error} -eq 1 ]]; then
echo "[!] Lint errors detected"
if [[ ${non_interactive} -eq 1 ]]; then
echo "[Non-interactive] Auto-fixing lint issues..."
# shellcheck disable=SC2086
"${lint_cmd}" ${CGW_LINT_FIX_ARGS} ${CGW_LINT_EXCLUDES} 2>&1 | tee -a "$logfile"
if [[ -n "${CGW_FORMAT_CMD}" ]]; then
# shellcheck disable=SC2086
"${CGW_FORMAT_CMD}" ${CGW_FORMAT_FIX_ARGS} ${CGW_FORMAT_EXCLUDES} 2>&1 | tee -a "$logfile"
if [[ -n "${CGW_FORMAT_CMD}" ]]; then
# shellcheck disable=SC2086 # Word splitting intentional: CGW_FORMAT_CHECK_ARGS/CGW_FORMAT_EXCLUDES contain multiple flags
format_output=$("${CGW_FORMAT_CMD}" ${CGW_FORMAT_CHECK_ARGS} ${CGW_FORMAT_EXCLUDES} 2>&1) || python_lint_error=1
if [[ -n "$format_output" ]] && [[ "$format_output" == *"would reformat"* ]]; then
echo "[FORMAT ERRORS]" | tee -a "$logfile"
echo "$format_output" | tee -a "$logfile"
fi
fi

if [[ ${staged_only} -eq 0 ]]; then
git add .
unstage_local_only_files
fi
log_section_end "LINT CHECK" "$logfile" "$python_lint_error"

# Re-check
python_lint_error=0
# shellcheck disable=SC2086
"${lint_cmd}" ${CGW_LINT_CHECK_ARGS} ${CGW_LINT_EXCLUDES} 2>&1 | tee -a "$logfile" || python_lint_error=1
if [[ ${python_lint_error} -eq 1 ]]; then
echo "[!] Lint errors detected"
if [[ ${non_interactive} -eq 1 ]]; then
echo "[Non-interactive] Auto-fixing lint issues..."
# shellcheck disable=SC2086 # Word splitting intentional: CGW_LINT_FIX_ARGS/CGW_LINT_EXCLUDES contain multiple flags
"${lint_cmd}" ${CGW_LINT_FIX_ARGS} ${CGW_LINT_EXCLUDES} 2>&1 | tee -a "$logfile"
if [[ -n "${CGW_FORMAT_CMD}" ]]; then
# shellcheck disable=SC2086 # Word splitting intentional: CGW_FORMAT_FIX_ARGS/CGW_FORMAT_EXCLUDES contain multiple flags
"${CGW_FORMAT_CMD}" ${CGW_FORMAT_FIX_ARGS} ${CGW_FORMAT_EXCLUDES} 2>&1 | tee -a "$logfile"
fi
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

commit_enhanced.sh still treats formatting as a sub-part of the code lint block. If a repo intentionally disables code lint via CGW_LINT_CMD="" but keeps formatting enabled (CGW_FORMAT_CMD set), the format check (and format auto-fix) will never run here—while check_lint.sh/push_validated.sh will run format independently. That inconsistency makes “format-only” setups behave differently depending on which CGW entrypoint is used.

Suggestion

Refactor the code-quality step to run format independently of CGW_LINT_CMD (mirroring check_lint.sh). One approach:

  • Track lint_error and format_error separately.
  • Run the lint block only if [[ -n "${CGW_LINT_CMD}" ]].
  • Run the format block only if [[ -n "${CGW_FORMAT_CMD}" ]].
  • Combine them for the overall decision (python_lint_error=1 if either fails).
  • In the auto-fix path, run only the corresponding fix commands that are configured.

Reply with "@CharlieHelps yes please" if you’d like me to add a commit implementing this refactor and updating/adding integration tests for the CGW_LINT_CMD="" + CGW_FORMAT_CMD!= "" case.

Comment thread scripts/git/commit_enhanced.sh Outdated
Comment on lines +246 to +289
if [[ -z "${CGW_LINT_CMD}" ]]; then
echo " (lint check skipped — CGW_LINT_CMD not set)"
else
log_section_start "LINT CHECK" "$logfile"

# Determine lint binary (venv or PATH)
local lint_cmd="${CGW_LINT_CMD}"
if [[ "${CGW_LINT_CMD}" == "ruff" ]]; then
get_python_path 2>/dev/null || true
if [[ -n "${PYTHON_BIN:-}" ]] && [[ -f "${PYTHON_BIN}/ruff${PYTHON_EXT:-}" ]]; then
lint_cmd="${PYTHON_BIN}/ruff${PYTHON_EXT:-}"
fi
fi
fi

local lint_output format_output
local lint_output format_output

# shellcheck disable=SC2086
lint_output=$("${lint_cmd}" ${CGW_LINT_CHECK_ARGS} ${CGW_LINT_EXCLUDES} 2>&1) || python_lint_error=1
if [[ -n "$lint_output" ]] && [[ "$lint_output" != *"All checks passed"* ]]; then
echo "[LINT ERRORS]" | tee -a "$logfile"
echo "$lint_output" | tee -a "$logfile"
fi

if [[ -n "${CGW_FORMAT_CMD}" ]]; then
# shellcheck disable=SC2086
format_output=$("${CGW_FORMAT_CMD}" ${CGW_FORMAT_CHECK_ARGS} ${CGW_FORMAT_EXCLUDES} 2>&1) || python_lint_error=1
if [[ -n "$format_output" ]] && [[ "$format_output" == *"would reformat"* ]]; then
echo "[FORMAT ERRORS]" | tee -a "$logfile"
echo "$format_output" | tee -a "$logfile"
# shellcheck disable=SC2086 # Word splitting intentional: CGW_LINT_CHECK_ARGS/CGW_LINT_EXCLUDES contain multiple flags
lint_output=$("${lint_cmd}" ${CGW_LINT_CHECK_ARGS} ${CGW_LINT_EXCLUDES} 2>&1) || python_lint_error=1
if [[ -n "$lint_output" ]] && [[ "$lint_output" != *"All checks passed"* ]]; then
echo "[LINT ERRORS]" | tee -a "$logfile"
echo "$lint_output" | tee -a "$logfile"
fi
fi

log_section_end "LINT CHECK" "$logfile" "$python_lint_error"

if [[ ${python_lint_error} -eq 1 ]]; then
echo "[!] Lint errors detected"
if [[ ${non_interactive} -eq 1 ]]; then
echo "[Non-interactive] Auto-fixing lint issues..."
# shellcheck disable=SC2086
"${lint_cmd}" ${CGW_LINT_FIX_ARGS} ${CGW_LINT_EXCLUDES} 2>&1 | tee -a "$logfile"
if [[ -n "${CGW_FORMAT_CMD}" ]]; then
# shellcheck disable=SC2086
"${CGW_FORMAT_CMD}" ${CGW_FORMAT_FIX_ARGS} ${CGW_FORMAT_EXCLUDES} 2>&1 | tee -a "$logfile"
if [[ -n "${CGW_FORMAT_CMD}" ]]; then
# shellcheck disable=SC2086 # Word splitting intentional: CGW_FORMAT_CHECK_ARGS/CGW_FORMAT_EXCLUDES contain multiple flags
format_output=$("${CGW_FORMAT_CMD}" ${CGW_FORMAT_CHECK_ARGS} ${CGW_FORMAT_EXCLUDES} 2>&1) || python_lint_error=1
if [[ -n "$format_output" ]] && [[ "$format_output" == *"would reformat"* ]]; then
echo "[FORMAT ERRORS]" | tee -a "$logfile"
echo "$format_output" | tee -a "$logfile"
fi
fi

if [[ ${staged_only} -eq 0 ]]; then
git add .
unstage_local_only_files
fi
log_section_end "LINT CHECK" "$logfile" "$python_lint_error"

# Re-check
python_lint_error=0
# shellcheck disable=SC2086
"${lint_cmd}" ${CGW_LINT_CHECK_ARGS} ${CGW_LINT_EXCLUDES} 2>&1 | tee -a "$logfile" || python_lint_error=1
if [[ ${python_lint_error} -eq 1 ]]; then
echo "[!] Lint errors detected"
if [[ ${non_interactive} -eq 1 ]]; then
echo "[Non-interactive] Auto-fixing lint issues..."
# shellcheck disable=SC2086 # Word splitting intentional: CGW_LINT_FIX_ARGS/CGW_LINT_EXCLUDES contain multiple flags
"${lint_cmd}" ${CGW_LINT_FIX_ARGS} ${CGW_LINT_EXCLUDES} 2>&1 | tee -a "$logfile"
if [[ -n "${CGW_FORMAT_CMD}" ]]; then
# shellcheck disable=SC2086 # Word splitting intentional: CGW_FORMAT_FIX_ARGS/CGW_FORMAT_EXCLUDES contain multiple flags
"${CGW_FORMAT_CMD}" ${CGW_FORMAT_FIX_ARGS} ${CGW_FORMAT_EXCLUDES} 2>&1 | tee -a "$logfile"
fi
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

commit_enhanced.sh still effectively couples formatting enforcement to whether CGW_LINT_CMD is set: when CGW_LINT_CMD="" (code lint disabled) but CGW_FORMAT_CMD is configured, the script prints “lint check skipped” and never runs the format check. That’s inconsistent with check_lint.sh (format runs independently) and makes “format-only” repos silently lose formatting protection at commit time.

Separately, the format invocation uses ${CGW_FORMAT_CMD} directly, while the lint path resolution special-cases ruff to use the .venv binary. If ruff is only available in .venv, lint will work but format can fail with command not found (or run the wrong global binary).

Suggestion

Refactor the “code quality” step so format runs independently of code lint (and resolve the format binary the same way as lint when CGW_FORMAT_CMD=ruff). For example:

# Resolve lint_cmd (only if CGW_LINT_CMD set)
local lint_cmd="${CGW_LINT_CMD}"
if [[ -n "${CGW_LINT_CMD}" && "${CGW_LINT_CMD}" == "ruff" ]]; then
  get_python_path 2>/dev/null || true
  if [[ -n "${PYTHON_BIN:-}" ]] && [[ -f "${PYTHON_BIN}/ruff${PYTHON_EXT:-}" ]]; then
    lint_cmd="${PYTHON_BIN}/ruff${PYTHON_EXT:-}"
  fi
fi

# Resolve format_cmd independently
local format_cmd="${CGW_FORMAT_CMD}"
if [[ -n "${CGW_FORMAT_CMD}" && "${CGW_FORMAT_CMD}" == "ruff" ]]; then
  get_python_path 2>/dev/null || true
  if [[ -n "${PYTHON_BIN:-}" ]] && [[ -f "${PYTHON_BIN}/ruff${PYTHON_EXT:-}" ]]; then
    format_cmd="${PYTHON_BIN}/ruff${PYTHON_EXT:-}"
  fi
fi

# Then run lint (if configured) and format (if configured) as separate checks.

If you’d like, I can put up a commit that (1) decouples format from CGW_LINT_CMD, (2) resolves ruff for format via .venv like lint does, and (3) updates/extends the Bats coverage for “format-only” setups. Reply with "@CharlieHelps yes please" and I’ll add it.

Comment thread scripts/git/_config.sh Outdated
Comment on lines +52 to +72
if [[ -f "${_CGW_CONF}" ]]; then
# shellcheck source=/dev/null
source "${_CGW_CONF}"
# Read .cgw.conf line-by-line, only applying variables not already set in the environment.
# This ensures env vars take priority AND derived values (e.g. CGW_PROTECTED_BRANCHES
# referencing CGW_TARGET_BRANCH) stay consistent with the values actually used.
while IFS= read -r _line; do
[[ "${_line}" =~ ^[[:space:]]*# ]] && continue # skip comments
[[ "${_line}" =~ ^[[:space:]]*$ ]] && continue # skip blank lines
# Only accept CGW_* assignment lines (optionally prefixed with export).
# Reject anything else to prevent eval of arbitrary shell statements.
if [[ "${_line}" =~ ^[[:space:]]*(export[[:space:]]+)?(CGW_[A-Z0-9_]+)= ]]; then
_cgw_var="${BASH_REMATCH[2]}"
# Only set if not already in environment (preserves env var priority)
if [[ -z "${!_cgw_var+x}" ]]; then
# shellcheck disable=SC2163 # eval required: _line contains "KEY=VALUE" or "export KEY=VALUE"
eval "${_line}"
fi
else
err "Ignoring unsupported line in .cgw.conf: ${_line}"
fi
done < "${_CGW_CONF}"
unset _line _cgw_var
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

_config.sh calls err when encountering an unsupported .cgw.conf line. That introduces an unnecessary dependency on _common.sh’s helper functions and makes _config.sh more fragile when sourced in isolation (e.g., unit tests or any future tooling that loads config without _common.sh).

Suggestion

Avoid calling err from _config.sh. Use a direct stderr write (or a tiny local _config_warn() helper) so _config.sh remains self-contained:

# inside the parser
printf '%s\n' "[WARN] Ignoring unsupported line in .cgw.conf: ${_line}" >&2

Reply with "@CharlieHelps yes please" if you’d like me to add a commit to decouple _config.sh from _common.sh in this spot (and adjust tests if needed).

@forkni
Copy link
Copy Markdown
Owner Author

forkni commented Apr 8, 2026

Addressed Charlie CI Round-4 Feedback

All 3 items resolved in commit 0af3b42:

# Issue Fix
1 commit_enhanced.sh format nested inside lint block — format-only configs silently broken Refactored code-quality section: lint and format now run as independent blocks. Each gated by its own [[ -n "${CGW_*_CMD}" ]] check. lint_error and format_error tracked separately, combined for auto-fix decisions. Format binary resolved via venv path (same as lint).
2 _config.sh calls err (from _common.sh) — not self-contained Replaced with printf '%s\n' "[WARN] ..." >&2 — no external dependency
3 (summary mention) create_pr.sh URL extraction — gh pr create outputs the URL directly on stdout; --json url is a gh pr view flag, not available on create. Current grep -oE approach preserved.

@forkni forkni merged commit f433951 into main Apr 8, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant