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
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:
./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=<your API 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: <sentinel>: http NNN — parseable |
Sentinels (library): redmine.ErrAuth, ErrNotFound, ErrValidation,
ErrServer, ErrUnreachable, ErrMalformedResponse — all comparable
with errors.Is.
Library surface (what the harness calls)
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
./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 | pending first PMO switch (credentials via ~/.creds/redmine.env) |
| Uploads / attachments | not in scope for v0 |
License: AGPLv3 (see LICENSE). Repository:
https://git.knownelement.com/ukrrs/mopac-redmine-go