# mopac-redmine-go A 100% Go client library and CLI (`mred`) for the Redmine REST API. Replaces the PMO's python glue scripts for every Redmine update: one static binary, zero third-party modules (stdlib only), machine-parseable JSON output, and an API key that never appears in flags, logs, or error strings. Status: 2026-08-29 — v0 complete and green: issues (list / show / create / update with notes-as-journals), versions, categories, relations, name-to-id resolution over server enumerations, 0600 env-file config, `-o json` on every command, exit codes 0/1/2 with one-line API-error stderr. Built and tested entirely against a fake Redmine (unit suite + end-to-end smoke); the live tracker (projects.knownelement.com) attaches with zero code change via `MRED_URL`/`MRED_KEY`. ## Architecture ```mermaid flowchart LR subgraph host[Host] M[mred CLI\nbin/mred] --config 0600 env --> M S[smoke/smoke.sh] end subgraph docker[Digest-pinned Docker builder — ALL dev] B[go build / vet / test] F[smoke/fakeredmine\nstateful in-memory Redmine] end subgraph wire[Library: package redmine] C[Client\nX-Redmine-API-Key header only] end M --> C C --> F C -.-> R[(real Redmine\nprojects.knownelement.com)] S --> B S --> F ``` All compilation, vetting, and testing runs inside the digest-pinned `golang:1.26-bookworm` builder (`./dev.sh`); the host never runs a Go toolchain. Tests and smoke talk only to the in-process/containerized fake; the real tracker is never contacted by the test suite. ## Quickstart (verified against the fake) This is the exact flow the smoke run (`./dev.sh smoke`) executes: ```sh ./dev.sh check # build + vet + test inside the Docker builder ./dev.sh smoke # boots fake Redmine on 127.0.0.1:8601, drives bin/mred # against a real tracker instead: export MRED_URL=https://projects.knownelement.com export MRED_KEY= # or: --config ~/.config/mred/env (0600) mred issue list -p MOPAC # open issues mred issue list -p MOPAC --status all --limit 100 mred issue show 506 --with journals mred issue create -p MOPAC -s "Subject" --tracker feature \ --priority immediate --category Secrets --version Beta \ --due 2026-08-31 --est 8 --desc - <<'EOF' ## Scope - body EOF mred issue update 506 --status "In Progress" --note "REPORT delivered" mred relation create 490 492 --type blocks mred version create -p MOPAC -n "Beta" --due 2026-08-31 --status open mred category create -p MOPAC -n "Secrets" ``` Add `-o json` to any command for machine output. Names (`--tracker feature`, `--priority immediate`, `--status done`, `--category`, `--version`) resolve case-insensitively against the server's own enumerations. ## Command reference | Command | Effect | Key flags | |---|---|---| | `issue list -p PROJ` | list issues (open by default) | `--status open\|all\|closed\|NAME`, `--version NAME`, `--limit N`, `-o json` | | `issue show ID` | one issue with description | `--with journals` | | `issue create -p PROJ -s SUBJECT` | create; returns the new issue | `--desc FILE\|-`, `--tracker`, `--priority`, `--category`, `--version`, `--due YYYY-MM-DD`, `--parent ID`, `--est HOURS`, `--note TEXT` | | `issue update ID` | partial update (only set fields are sent) | `--status`, `--priority`, `--category`, `--version`, `--due`, `--done-ratio 0-100`, `--desc FILE\|-`, `--note TEXT` (journal) | | `version list -p PROJ` | roadmap milestones | `-o json` | | `version create -p PROJ -n NAME` | create milestone (sharing=descendants) | `--due`, `--status open\|closed` | | `category list -p PROJ` | issue categories | `-o json` | | `category create -p PROJ -n NAME` | create category | | | `relation create FROM TO` | relate two issues | `--type blocks\|relates` | | `help` | usage | | Flags may appear before or after positionals (`mred issue update 506 --note X` and `mred relation create 490 492 --type blocks` both parse). ## Configuration | Source | Keys | Discipline | |---|---|---| | environment | `MRED_URL`, `MRED_KEY` | env wins over file | | `--config PATH` (also `$MRED_CONFIG`, default `~/.config/mred/env`) | `MRED_URL=...`, `MRED_KEY=...` | file must be 0600 or stricter; refused before any read; parsed in pure Go (no sourcing/expansion) | The API key travels only in the `X-Redmine-API-Key` header. Response bodies are never surfaced in error strings — the fake Redmine deliberately echoes the presented key in error bodies, and the test suite proves nothing leaks (see `TestKeyNeverLeaks` and the smoke redaction pass). ## Exit codes and stderr | Code | Meaning | stderr shape | |---|---|---| | 0 | ok | — | | 1 | usage / config error | one line, names the problem (never values) | | 2 | API error | one line: `mred: redmine: : http NNN` — parseable | Sentinels (library): `redmine.ErrAuth`, `ErrNotFound`, `ErrValidation`, `ErrServer`, `ErrUnreachable`, `ErrMalformedResponse` — all comparable with `errors.Is`. ## Library surface (what the harness calls) ```go import "git.knownelement.com/ukrrs/mopac-redmine-go/redmine" c := redmine.New(redmine.Config{BaseURL: url, APIKey: key}) issues, total, _ := c.ListIssues(ctx, redmine.IssueFilter{Project: "MOPAC"}) issue, _ := c.GetIssue(ctx, 506, true) // + journals created, _ := c.CreateIssue(ctx, redmine.IssueParams{Project: "MOPAC", Subject: "…"}) _ = c.UpdateIssue(ctx, 506, redmine.IssueParams{StatusID: 3, Notes: "REPORT delivered"}) versions, _ := c.ListVersions(ctx, "MOPAC") cats, _ := c.ListCategories(ctx, "MOPAC") rel, _ := c.CreateRelation(ctx, 490, 492, "blocks") // name resolution shared with the CLI: id, _ := c.StatusIDByName(ctx, "done") // 3 id, _ = c.VersionIDByName(ctx, "MOPAC", "Beta") // milestone id id, _ = c.CategoryIDByName(ctx, "MOPAC", "Secrets") ``` `UpdateIssue` sends only the fields you set (partial update — untouched attributes are never clobbered). All methods take a `context.Context`. ## Development ```sh ./dev.sh build|vet|test|check|smoke|shell # everything runs in the digest-pinned builder make build|vet|test|check|smoke # same, Makefile front door ``` Test suite: table-driven round-trips against the stateful fake (`internal/fakeredmine`): create/update/note/relations/versions, list filters, name resolution, error mapping (401/403/404/422/5xx → sentinels), unreachable/malformed handling, and key-redaction. ## Status | Area | State | |---|---| | Library (issues, versions, categories, relations, enums) | done, green | | CLI surface (task spec in Redmine #506) | done, green | | Fake-Redmine test suite | done, green (unit + smoke) | | Key redaction | done, tested (fake echoes the key; nothing leaks) | | Dev harness (Docker-only builds) | done (`dev.sh`/`Makefile`) | | Live-tracker verification | reads verified 2026-08-29 (`issue list` text+json vs projects.knownelement.com, exit 0; live writes await PMO acceptance) | | Uploads / attachments | not in scope for v0 | License: AGPLv3 (see `LICENSE`). Repository: https://git.knownelement.com/ukrrs/mopac-redmine-go