# 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): ```sh 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 --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.