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

S
Description
mred: stdlib-only Redmine REST client library + CLI (AGPLv3)
Readme AGPL-3.0
130 KiB
Languages
Go 93.2%
Shell 6.5%
Makefile 0.3%