Files
mopac-redmine-go/README.md
T

170 lines
7.0 KiB
Markdown

# 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=<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)
```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