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 theX-Redmine-API-Keyheader. - 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.envkey-renamed to a 0600--configfile,mred issue list -p MOPAC --limit 3returned 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:
--noteis 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 jsonis 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)
- Run the setup block above;
mred issue list -p MOPAC --limit 5returns live issues (exit 0). (Pre-verified 2026-08-29: this exact command returned 522/521/519 from the live tracker.) - 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. - 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.