Files
mopac-redmine-go/README.md
T
mrcharles 5abd352aa5 docs(readme): document version update
Add the command-reference row, a quickstart line, the library-surface
signatures, and a status-table note that version updates are done and
green.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-29 08:12:16 -05:00

7.5 KiB

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 version update 8 --status closed --due 2026-09-15
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
version update ID partial update (only set flags are sent; server's 204 empty reply never parsed) --status open|closed, --due YYYY-MM-DD, --name NAME, -o json (re-fetches)
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")
ver, _         := c.GetVersion(ctx, 8)                      // one milestone
_              = c.UpdateVersion(ctx, 8, redmine.VersionParams{Status: "closed"}) // PUT -> 204, partial
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 and UpdateVersion send only the fields you set (partial update — untouched attributes are never clobbered); UpdateVersion swallows Redmine's empty 204 reply. 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 incl. update, 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