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
175 lines
7.5 KiB
Markdown
175 lines
7.5 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 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)
|
|
|
|
```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")
|
|
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
|
|
|
|
```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 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
|