Files
mopac-pmo/inbox-pmo/REPORT-20260829-1400-redmine-cli.md
T

8.0 KiB

REPORT — mopac-redmine-go (mred): Redmine Go CLI delivered

Task: TASK-20260829-1400-redmine-cli (Redmine #506, top priority by direct order 2026-08-29) Delivered: 2026-08-29 12:05 Repo: https://git.knownelement.com/ukrrs/mopac-redmine-go (public, AGPLv3) Clone: ~/projects/meta/MOPAC/redmine-go (origin main, 7 commits) Status: v0 complete, green, pushed. Ready for PMO cutover on acceptance.

What landed

Stdlib-only Go module git.knownelement.com/ukrrs/mopac-redmine-go: library package redmine (Client + sentinel errors, usable by the harness directly — no copy-paste surface), CLI mred (internal/cli + cmd/mred), stateful fake Redmine (internal/fakeredmine), 0600-disciplined config loader (internal/config). ALL dev work ran inside the digest-pinned golang:1.26-bookworm builder via ./dev.sh/Makefile (host never runs a toolchain). TDD red/green throughout; atomic conventional commits pushed to origin main.

Commit Content
4b9aa5f Seed: AGPLv3 LICENSE (first commit)
c3b86b1 Dev harness (digest-pinned Docker builder)
a3c83bd Config: MRED_* env + 0600 env-file loading
20dbcf6 Redmine REST client + fake-server test suite
d98f0ae mred CLI (flags accepted after positionals)
bbd85fa End-to-end smoke vs containerized fake
4ad9f6f README to docs standard

Command surface

Command Effect Key flags
mred issue list -p MOPAC list issues (open by default) --status open|all|closed|NAME, --version NAME, --limit N, -o json
mred issue show ID full issue + description --with journals
mred issue create -p PROJ -s SUBJECT create; returns new issue --desc FILE|-, --tracker, --priority, --category, --version, --due DATE, --parent ID, --est HOURS, --note TEXT
mred issue update ID partial update (only set fields sent) --status, --priority, --category, --version, --due, --done-ratio, --desc FILE|-, --note TEXT (= journal)
mred version list -p PROJ / version create -p PROJ -n NAME milestones --due DATE, --status open|closed (sharing=descendants)
mred category list -p PROJ / category create -p PROJ -n NAME categories
mred relation create FROM TO --type blocks|relates issue relations

Names resolve case-insensitively against the server's own enumerations (--tracker feature -> id 2, --priority immediate -> id 5, --status done, --category/--version by name; no id lookups in PMO scripts ever again). Flags parse before OR after positionals. Exit codes: 0 ok, 1 usage/config, 2 API error — stderr is one parseable line: mred: redmine: not found: http 404.

Test results

  • Unit suite: 62 passing tests (34 functions + 28 table/subtests) across library round-trips, list filters, notes-as-journals, relations, versions, categories, name resolution, error mapping (401/403/404/422/5xx -> sentinels; unreachable; malformed), config 0600 discipline.
  • Key redaction: the fake DELIBERATELY echoes the presented API key in every error body; TestKeyNeverLeaks (library) + the smoke redaction pass (CLI) prove the key appears nowhere except the X-Redmine-API-Key header.
  • End-to-end smoke: smoke: OK — builds in the builder, boots the fake in a container on 127.0.0.1:8601, drives the real binary through a 0600 env file: version/category create+list, issue create with full flags + desc from stdin (-o json), list + filters, update with status/done-ratio/note then show-with-journals, relation create, 404/401/loose-file failure paths with exact exit codes, redaction sweep. Only the exact container ID is ever removed; no broad pkill.
  • ./dev.sh check (build+vet+test) green at HEAD.
  • Live tracker verified (read-only) 2026-08-29: with ~/.creds/redmine.env key-renamed to a 0600 --config file, mred issue list -p MOPAC --limit 3 returned real issues (522/521/519, text and -o json, exit 0) from projects.knownelement.com — zero code change from the fake-tested path. Live WRITES await PMO acceptance (step 3 below).

PMO replacement mappings (python ops today -> mred one-liners)

Setup once (then every row below is one command):

sudo install -m 755 ~/projects/meta/MOPAC/redmine-go/bin/mred /usr/local/bin/mred
install -d -m 700 ~/.config/mred
sed -e 's/^REDMINE_URL=/MRED_URL=/' -e 's/^REDMINE_API_KEY=/MRED_KEY=/' \
    ~/.creds/redmine.env > ~/.config/mred/env && chmod 600 ~/.config/mred/env
mred issue list -p MOPAC --limit 5   # live verify vs projects.knownelement.com
Python op today (file) mred one-liner
Create roadmap version: POST /projects/MOPAC/versions.json {"name","due_date","status","sharing":"descendants"} (roadmap-buildout.py §1) mred version create -p MOPAC -n "Beta" --due 2026-08-31 --status open
List versions to map name->id (roadmap-buildout.py §1 fallback) mred version list -p MOPAC -o json (name->id lookup no longer needed; --version takes the NAME)
Create category (roadmap-buildout.py §2) mred category create -p MOPAC -n "Quota & Backpressure"
List categories (roadmap-buildout.py §2 fallback) mred category list -p MOPAC -o json
Update issue category+version+estimate: PUT /issues/490.json {"category_id","fixed_version_id","estimated_hours"} (roadmap-buildout.py §3) mred issue update 490 --category "Quota & Backpressure" --version Beta --est 8
Relation: POST /issues/490/relations.json {"issue_to_id":492,"relation_type":"blocks"} (roadmap-buildout.py §4) mred relation create 490 492 --type blocks
Create child issue with tracker/priority/category/version/est/desc (roadmap-buildout.py §5) mred issue create -p MOPAC -s "Google suite MCP/CLI" --tracker task --priority normal --category Integrations --version "Phase 3 - Integrations" --est 8 --parent 498 --desc - (desc via heredoc/file)
Fetch issue 517, rewrite description at marker, PUT desc + note (amend-517.py) mred issue show 517 -o json | jq -r '.issue.description' | { editor pipeline } > /tmp/d.md && mred issue update 517 --desc /tmp/d.md --note "Hard requirement added: ..."
Append-desc + note to 497 (finalize-497.py §1) mred issue show 497 -o json | jq -r '.issue.description' | head ... > /tmp/d.md && mred issue update 497 --desc /tmp/d.md --note "FINAL model: ..."
Cross-issue note (finalize-497.py §2, note on 517) mred issue update 517 --note "Clarification per #497 final: ..."
Read category/version maps then create identity ticket (file-identity-ticket.py) mred issue create -p MOPAC -s "Two-level identity model: Linux account (runtime) + Cloudron accounts (acting)" --tracker feature --priority urgent --category Infrastructure --version Production --est 5 --desc identity-desc.md
Generic list/read the PMO does inline (status sweeps) mred issue list -p MOPAC --status all -o json / mred issue show ID --with journals -o json

Notes for the cutover:

  • --note is idempotent-safe to skip: omit it and no journal entry is written. Notes are one-per-call (Redmine journals are append-only).
  • Text output is stable/grep-able (ID STATUS SUBJECT @Version); -o json is the machine contract ({"issues":[...]} etc.).
  • No python process, no urllib timeout handling, no key material in scripts: the key sits in a 0600 env file mred refuses to read looser.

Acceptance checklist (PMO)

  1. Run the setup block above; mred issue list -p MOPAC --limit 5 returns live issues (exit 0). (Pre-verified 2026-08-29: this exact command returned 522/521/519 from the live tracker.)
  2. Spot-map one recurring op (e.g. the finalize-497 note pattern) with mred issue update <id> --note "cutover test" and verify the journal in the web UI.
  3. On pass: python glue in ~/.coordinate/scripts is retired for Redmine writes; comment/close #506 via mred issue update 506 --status done --done-ratio 100 --note "Delivered: ukrrs/mopac-redmine-go v0".

Zero downtime: python glue keeps working until step 3 — mred adds no server-side change; both clients can run side by side indefinitely.