Skip to content

feat(openemr-cmd): wrap pre-commit tooling, add prek-in-container dispatch#718

Open
bradymiller wants to merge 3 commits into
openemr:masterfrom
bradymiller:devops-wt-sync-dev-tools-openemrcmd
Open

feat(openemr-cmd): wrap pre-commit tooling, add prek-in-container dispatch#718
bradymiller wants to merge 3 commits into
openemr:masterfrom
bradymiller:devops-wt-sync-dev-tools-openemrcmd

Conversation

@bradymiller
Copy link
Copy Markdown
Member

Summary

Adds in-container access to every pre-commit hook from OpenEMR's .pre-commit-config.yaml so developers can git commit from the host without installing PHP, Node, Python, codespell, pre-commit, or actionlint locally. The openemr flex container becomes the execution environment; openemr-cmd is the bridge.

  • openemr-cmd wrappers for each pre-commit composer script: cps (codespell), ccc (conventional-commits-check), crc (require-checker), cv (composer-validate), cn (composer-normalize), cnf (composer-normalize-fix), cck (composer-checks), cq (code-quality). First five wired into clean-sweep so the existing aggregate stays comprehensive.
  • openemr-cmd prek <args>: passthrough to in-container pre-commit. Substitutes actionlint-dockeractionlint-system at invocation time (DinD isn't available from inside the container); the repo's .pre-commit-config.yaml is untouched and host-side prek/pre-commit usage continues to work unchanged. cwd-aware container resolution (wt_resolve_container_from_cwd) so a commit fired from worktree foo always dispatches to foo's container.
  • openemr-cmd pi / pu (prek-install / prek-uninstall): manage host-side .git/hooks/{pre-commit,commit-msg} shims. One install on the primary repo covers every linked worktree (current and future) via gitcommondir. Refuses to clobber non-managed hooks without --force. Runtime safeguard rejects invocation via -d <container> or worktree exec with a clear error.
  • Container packages relocated: chromium, chromium-chromedriver, py3-codespell, plus the new pre-commit and actionlint, now installed at top of openemr.sh under DEVELOPER_TOOLS=yes gate (idempotent via apk info -e guards). Previously inside NEED_COMPOSER_BUILD=true, which meant they didn't survive docker compose down --keep-volumes && up (vendor preserved → block skipped → packages missing on the recreated container).
  • Bumps openemr-cmd VERSION to 1.0.31. Fixes a stale openemr-cmd-h filter line count (was 9, now 23).

End-to-end workflow this unlocks

For a contributor with only Docker installed on their host:

openemr-cmd worktree add my-feature -b              # one-time per feature
openemr-cmd pi                                       # one-time per clone
# edit files
git add path/to/file.php
git commit -m "feat: ..."
# → host hook fires → openemr-cmd prek run --hook-stage pre-commit
# → cwd-aware resolution dispatches to my-feature's container
# → pre-commit runs inside container against staged files
# → commit aborts on any failure, succeeds otherwise

Manual invocation paths remain:

  • openemr-cmd prek run --all-files — full-codebase check
  • openemr-cmd cq — full composer code-quality suite (direct composer path, bypasses prek)
  • openemr-cmd worktree exec <branch> prek run --all-files — target a specific worktree from anywhere

Test plan

openemr-cmd smoke checks

  • openemr-cmd --version reports 1.0.31
  • openemr-cmd --help shows the pre-commit-management: section with prek, pi/prek-install, pu/prek-uninstall entries, plus the eight new short-form wrappers under php-management
  • openemr-cmd-h php displays all 23 php-management entries (was truncated before due to stale filter count)

Per-tool wrappers (run inside any DEVELOPER_TOOLS=yes container; openemr-cmd from host)

  • openemr-cmd cps → runs codespell, no crash on warnings
  • openemr-cmd cv → reports composer.json validation
  • openemr-cmd cn → dry-run normalization, reports diff or "already normalized"
  • openemr-cmd crc → composer-require-checker output
  • openemr-cmd cq → runs the full composer code-quality suite

prek dispatcher

  • openemr-cmd prek --version reports the in-container pre-commit version
  • openemr-cmd prek run --all-files executes the full hook suite; actionlint runs without DinD
  • openemr-cmd prek run phpstan runs the single hook
  • Inside the openemr container, /tmp/openemr-cmd-pre-commit-config.yaml shows actionlint-system (not -docker)
  • From inside worktree foo, openemr-cmd prek run dispatches to foo's container (verify via docker ps while it runs)

Hook installation

  • openemr-cmd pi from the primary repo writes pre-commit and commit-msg shims to <primary>/.git/hooks/
  • Both shim files contain the __openemr_cmd_prek_hook__ marker
  • git commit from a worktree fires the shim and dispatches into that worktree's container
  • git commit from the primary fires the shim and dispatches into the primary's container
  • openemr-cmd pi refuses to overwrite a non-managed pre-existing hook; pi --force backs it up to *.bak.<timestamp> and installs
  • openemr-cmd pu removes only marker-bearing hooks; skips non-managed ones with a notice

Runtime safeguards

  • openemr-cmd -d <any-container> pi exits 1 with "host-only command" error (does not enter container)
  • openemr-cmd worktree exec <branch> pi produces the same error transitively
  • Same for pu/prek-uninstall

Container package persistence (the NEED_COMPOSER_BUILD fix)

  • After openemr-cmd worktree down --keep-volumes <branch> && openemr-cmd worktree up <branch>, all of chromium-browser, codespell, pre-commit, and actionlint resolve to installed binaries inside the new container
  • On subsequent container starts with packages already present, no apk install runs (verify the apk info -e short-circuit)

Compatibility

  • Host-based prek install followed by git commit still works for users who haven't run openemr-cmd pi (no change to host workflow)
  • CI continues to pass; in particular composer code-quality and shellcheck don't regress

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings May 13, 2026 16:58
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR extends openemr-cmd to act as a host-side bridge into the Flex container for pre-commit tooling, enabling contributors to run the repository’s pre-commit hooks (and related composer checks) without installing the underlying runtimes locally. It also adjusts the Flex container startup so required dev-only packages persist across container recreation scenarios.

Changes:

  • Add openemr-cmd prek dispatcher (in-container pre-commit) plus host-only prek-install/prek-uninstall hook shims; add short wrapper commands for several composer-based checks.
  • Extend the Flex devtools entrypoint to expose new composer commands and include key ones in clean-sweep.
  • Move installation of dev-only Alpine packages (chromium, codespell, pre-commit, actionlint) earlier in openemr.sh under the DEVELOPER_TOOLS=yes gate; update openemr-cmd-h filter count and bump openemr-cmd version.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
utilities/openemr-cmd/openemr-cmd-h Updates php-management help filter line count to reflect additional commands.
utilities/openemr-cmd/openemr-cmd Adds worktree-cwd-aware container resolution, pre-commit dispatch (prek), host hook install/uninstall commands, new PHP/composer wrappers, and bumps version.
docker/openemr/flex/utilities/devtools Adds devtools commands for composer-based pre-commit checks and wires key ones into clean-sweep.
docker/openemr/flex/openemr.sh Installs dev-only Alpine packages early (idempotent) to avoid missing tooling after container recreation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread utilities/openemr-cmd/openemr-cmd Outdated
cfg=/tmp/openemr-cmd-pre-commit-config.yaml
if [ -f .pre-commit-config.yaml ]; then
sed "s/actionlint-docker/actionlint-system/g" .pre-commit-config.yaml > "${cfg}"
exec pre-commit "$@" --config "${cfg}"
Comment thread utilities/openemr-cmd/openemr-cmd Outdated
Comment on lines +1907 to +1910
cfg=/tmp/openemr-cmd-pre-commit-config.yaml
if [ -f .pre-commit-config.yaml ]; then
sed "s/actionlint-docker/actionlint-system/g" .pre-commit-config.yaml > "${cfg}"
exec pre-commit "$@" --config "${cfg}"
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

Comment thread utilities/openemr-cmd/openemr-cmd Outdated
Comment on lines +189 to +192
docker ps \
--filter "label=com.docker.compose.project=openemr-${slug}" \
--filter "label=com.docker.compose.service=openemr" \
--format "{{.ID}}" | head -n 1
Comment on lines +894 to +906
#!/bin/sh
# openemr-cmd-managed commit-msg hook -- do not edit by hand
# ${PREK_HOOK_MARKER}
exec openemr-cmd prek run --hook-stage commit-msg --commit-msg-filename "\$1"
HOOKEOF
;;
*)
cat > "${target}" <<HOOKEOF
#!/bin/sh
# openemr-cmd-managed pre-commit hook -- do not edit by hand
# ${PREK_HOOK_MARKER}
exec openemr-cmd prek run --hook-stage pre-commit
HOOKEOF
Comment thread utilities/openemr-cmd/openemr-cmd Outdated
return 1
fi
local backup
backup="${target}.bak.$(date +%s)"
bradymiller and others added 3 commits May 14, 2026 23:00
…patch

Adds in-container access to every pre-commit hook from .pre-commit-config.yaml
so developers can `git commit` from the host without installing PHP, Node,
Python, codespell, pre-commit, or actionlint locally. The openemr flex
container becomes the execution environment; openemr-cmd is the bridge.

devtools subcommands and openemr-cmd wrappers for each pre-commit composer
script: codespell (cps), conventional-commits-check (ccc), require-checker
(crc), composer-validate (cv), composer-normalize (cn), composer-normalize-fix
(cnf), composer-checks (cck), code-quality (cq). First five are also wired
into clean-sweep so the existing aggregate stays comprehensive.

prek dispatcher (`openemr-cmd prek <args>`) is a passthrough to in-container
pre-commit. Substitutes actionlint-docker -> actionlint-system at invocation
time so the actionlint hook works without DinD; the repo's
.pre-commit-config.yaml is untouched and host-side prek/pre-commit usage is
unaffected. Resolves the target container via cwd
(wt_resolve_container_from_cwd) so a commit fired from worktree foo
dispatches to foo's container, not whichever worktree container happens to
be running.

prek-install (pi) and prek-uninstall (pu) manage host-side
.git/hooks/{pre-commit,commit-msg} shims. Linked worktrees share gitcommondir,
so a single install on the primary covers every current and future worktree.
Both refuse to clobber non-managed hooks without --force and reject
invocation via '-d <container>' or 'worktree exec' with a clear error.

Container packages (actionlint, pre-commit, py3-codespell, plus the existing
chromium/chromium-chromedriver) now installed at top of openemr.sh under the
DEVELOPER_TOOLS=yes gate. Previously these lived inside the NEED_COMPOSER_BUILD
branch, so they did not survive `docker compose down --keep-volumes && up`
(vendor preserved -> block skipped -> packages missing). Moved out and
guarded with `apk info -e` so they re-install on container recreation but
are no-ops on subsequent starts.

Bumps openemr-cmd VERSION to 1.0.31. Updates openemr-cmd-h's php-management
filter line count from a stale 9 to 23 so all entries display.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…atch

* SC2312 (line 185): process substitution masked jq's return value;
  add explicit '|| true' so the helper's empty-state handling is
  unambiguous to readers and to shellcheck.

* SC2155 (line 885): split 'local backup="..."' so the assignment's
  exit code isn't masked by the 'local' builtin.

* SC2310 (four sites in cmd_prek_install / cmd_prek_uninstall):
  drop 'func || exit 1' and 'if ! func' patterns (both of which
  disable set -e inside the called function) in favor of bare
  assignments that propagate the function's exit code through the
  script's set -euo pipefail.

* --config placement (Copilot): pre-commit parses '--config' at the
  top-level parser. Trailing placement (after the subcommand) gave
  "unrecognized arguments". Move to precede "$@".

* /tmp config race (Copilot): parallel openemr-cmd prek invocations
  in the same container could clobber each other's substituted
  config mid-run. Use mktemp for a unique path per invocation and
  trap-clean up on EXIT/INT/TERM.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docker ps in wt_resolve_container_from_cwd swallows daemon-unreachable
  errors (2>/dev/null) and forces a zero exit (|| true) so the caller can
  see an empty result rather than having 'set -o pipefail' kill the script
  before the dispatcher's own "no running openemr container" handling can
  run.

* Hook shims bake in the absolute path to the openemr-cmd binary that
  installed them ($0 resolved via realpath, with command -v fallback).
  Git frequently invokes hooks with a reduced PATH (especially from GUI
  clients), so 'exec openemr-cmd ...' could fail when openemr-cmd lives
  in a user-local bindir like ~/bin or ~/.local/bin.

* Backup filename for clobbered hooks uses mktemp instead of date +%s,
  so rapid 'prek-install --force' re-runs within the same second don't
  overwrite each other's backups. mktemp is portable across macOS BSD
  and Linux GNU coreutils (date +%s%N is GNU-only).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bradymiller bradymiller force-pushed the devops-wt-sync-dev-tools-openemrcmd branch from 480537a to 0904563 Compare May 15, 2026 06:01
composer phpstan-baseline
fi

if [ "$1" = "codespell" ] || [ "$1" = "clean-sweep" ]; then
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggest case/esac for this section, instead of if. There also really ought to be a way to avoid duplicating that cd. A shell function perhaps?


while IFS=$'\t' read -r b d; do
[[ -z "${b}" ]] && continue
local d_real
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can hoist this declaration

[[ -z "${b}" ]] && continue
local d_real
d_real=$(realpath -m "${d}" 2>/dev/null) || continue
if [[ "${d_real}" == "${toplevel_real}" ]]; then
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Recommend single equals. Just a style thing here, but double equals has subtle breakage when dealing with some shells. (Only time you need == in bash is in arithmetic context.)

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.

3 participants