Harden portable Redmine CLI runtime and cache writes

This commit is contained in:
Jason Thistlethwaite
2026-10-18 03:18:36 -04:00
parent f6915face2
commit 2e686215ed
19 changed files with 1353 additions and 224 deletions
+87
View File
@@ -0,0 +1,87 @@
# Redmine CLI runtime
The live wrappers are deliberately short:
```bash
bin/rcontacts-live search acme
bin/rtickets-live search --where 'assigned_to IS NULL'
bin/rtl search --where 'assigned_to IS NULL' # short live alias
bin/rtd search --where 'assigned_to IS NULL' # explicit LAN/dev lane
```
`--help` works without credentials. The wrappers resolve configuration independently of the current working directory and pass ordinary arguments unchanged to `redmine_contacts.py` or `redmine_tickets.py`.
## Diagnose configuration without API traffic
```bash
bin/rcontacts-live --doctor
bin/rtickets-live --show-config
bin/rtl --doctor-nmi
```
Diagnostics show effective paths and each setting’s source. Secrets are never printed. `network_checked: false` means readiness is local; it does not prove remote authentication.
## Precedence and files
Highest priority wins:
1. wrapper CLI options;
2. exported environment;
3. selected Redmine env file;
4. ticket JSON/YAML `--config` values;
5. defaults.
Select an env file with `--env-file PATH` or `REDMINE_ENV_FILE`. Otherwise the first existing file is used:
- live: checkout `.env`, legacy `bin/.env`, `~/.redmine.live`;
- dev: checkout `.env.dev`, `bin/.env.dev`, `~/.redmine.dev`.
Dev never falls back to the live file. An explicitly selected missing, empty, or malformed file is an error. Files use `NAME=value` and are parsed as data; shell commands are not evaluated.
```text
REDMINE_API_KEY required for API operations
REDMINE_BASE_URL live default https://rm.ldrprep.com
REDMINE_PROJECT default customer-service
REDMINE_BASE_CACHE common mutable-data root
REDMINE_CONTACTS_CACHE exact full-CRM snapshot JSON file
REDMINE_TICKETS_CACHE exact ticket-cache directory
REDMINE_LOCK_TIMEOUT writer wait, seconds; default 60
REDMINE_ENV_FILE explicit env profile
REDMINE_API_KEY_FILE file containing only the Redmine key
```
NMI bridge variables (`NMI_CLI`, `NMI_ENV_FILE`, `NMI_MODE`, endpoint/credential variables) pass through to PHP. `rtl` defaults NMI to production; `rtd` defaults it to dev. `--doctor-nmi` runs the actual PHP resolver offline.
## Cache layout
`REDMINE_BASE_CACHE` chooses the common root, defaulting to checkout `cache/` live and `cache-dev/` dev.
- Tickets normally use `<root>/redmine_tickets/`.
- Other endpoint/project combinations get deterministic distinct ticket directories.
- Fresh contacts setups use `<root>/redmine_contacts/<host>_<project>.json`.
- If historical `<root>/redmine_contacts` is already a **file** for normal live use, the wrapper preserves and uses it. There is no automatic migration.
- Tickets’ `<ticket-cache>/contacts/` mirror is a different per-contact format, not the full contacts snapshot.
Exact `--cache-dir DIR` (tickets) and `--cache FILE` (contacts) win. Contacts rejects a cache directory because `redmine_contacts.py` expects one JSON file.
`ticket-contact` prints a shell-quoted full-CRM handoff that retains endpoint, project, env profile, API-key-file context, and cache file without putting a key in argv.
## Concurrency and cron
Writers use POSIX advisory locks around read/modify/write transactions and unique same-directory temporary files followed by atomic replacement. A process’s death releases its kernel lock; the persistent `.lock` file is only an inode anchor. A later holder removes matching temporary residue left by a killed writer.
Contention is visible on stderr. After `REDMINE_LOCK_TIMEOUT`, the process exits nonzero rather than making a cron failure silent.
```cron
REDMINE_ENV_FILE=/etc/ldr/redmine-live.env /opt/redmine/bin/rtickets-live sync --remote-updated-since 6h --run-log --quiet
```
## Completion and safety
```bash
source bin/redmine-runtime-completion.bash
```
This registers all four wrappers and includes the established contacts completion surface.
The wrappers do not weaken write safety: production ticket writes still require `--allow-prod-writes`; customer email still requires its long confirmation; NMI retains its independent mutation gates.