2 Commits

Author SHA1 Message Date
Jan Nedbal
6e71ddb635 docs: add changelog with phase 1 notes
Seed CHANGELOG.md in Keep a Changelog format, document every Phase 1
subcommand plus the --json flag placement fix as the initial 0.1.0
entry. Future minors and patches add sections above this one.
2026-04-14 19:21:55 +02:00
Jan Nedbal
362bd6ae7c cli: accept --json/-v/-q on every subcommand, not only top level
Before this change, only the top-level parser defined --json, -v and -q.
Argparse processes arguments left-to-right and hands off to the subparser
after seeing the subcommand name, so the idiomatic

    metin-release release inspect --source X --json

failed with 'unrecognized arguments: --json'. Users had to write

    metin-release --json release inspect --source X

which is the opposite of what every modern CLI does. Attach a shared
--json/-v/-q flag set to every subparser via a small helper. Same dest
means the last occurrence on the command line wins, which is the intuitive
behaviour. Both placements are now accepted; tests unchanged.
2026-04-14 19:04:55 +02:00
2 changed files with 77 additions and 11 deletions

50
CHANGELOG.md Normal file
View File

@@ -0,0 +1,50 @@
# Changelog
All notable changes to `metin-release-cli` are documented here.
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versions
follow semantic versioning: major for incompatible CLI or JSON contract changes,
minor for new subcommands or additive flags, patch for fixes that preserve the
contract.
## [0.1.0] - 2026-04-14
First Phase 1 drop. Implements the minimum asset release path the
`metin-release-cli-plan.md` calls for — everything the launcher contract needs
to publish a signed manifest and content-addressed blob tree.
### Added
- `metin-release` command, `python -m metin_release` module entry, and
`metin-release --version` resolving via `importlib.metadata`
- Top-level and per-subcommand `--json`, `-v`, `-q` flags (both placements work)
- Structured result envelope and exit-code contract (`0/1/2/3/4`) from the plan
- `release inspect` — scan a client source root and report counts + flags
- `release build-manifest` — wrap `make-manifest.py` and report manifest stats
- `release sign` — wrap `sign-manifest.py` and verify 64-byte output
- `release diff-remote` — HEAD blob URLs and report the missing set
- `release upload-blobs` — rsync push blobs + archive to a target, with
`--dry-run` and `--yes`, excluding `manifest.json*` so promote stays atomic
- `release promote` — upload only `manifest.json` and `manifest.json.sig`
- `release verify-public` — fetch + Ed25519-verify the published manifest,
optional `--sample-blobs N` sha256 re-check
- `release publish` — composite of build-manifest → sign → upload-blobs →
promote → verify-public with per-stage timings and short-circuit on failure
- Error hierarchy in `errors.py` mapping each exception class to the right
exit code and a stable `error_code` string for machine parsing
- pytest suite with 18 tests covering dispatch, inspect, build-manifest, sign,
diff-remote (via a local HTTPServer fixture), and the full publish composite
against a local rsync target
- `docs/cli.md` man-page-style reference
### Notes
- Runtime deps pinned to `cryptography` and `requests` only. Dev deps are
`pytest` + `pytest-mock`.
- `make-manifest.py` and `sign-manifest.py` are discovered by convention at
`scripts/make-manifest.py` / `scripts/sign-manifest.py` relative to the CLI
repo, or overridden via `METIN_RELEASE_MAKE_MANIFEST` and
`METIN_RELEASE_SIGN_MANIFEST` env vars.
- Phase 2 (`erp …` subcommands), Phase 3 (MCP wrapper), Phase 4 (`m2pack …`
subcommands) and Phase 5 (`launcher publish`) are explicitly out of scope for
this release and will land in future minors.

View File

@@ -26,15 +26,27 @@ from .workspace import Context
CommandFn = Callable[[Context, argparse.Namespace], Result]
def _add_common_flags(p: argparse.ArgumentParser) -> None:
"""Attach --json / -v / -q to a parser.
These flags are accepted both at the top level and on each subcommand so
that users can write either `metin-release --json release inspect ...` or
the more common `metin-release release inspect ... --json`. The shared
`dest` means whichever occurrence argparse sees last wins, which is the
intuitive "last flag on the command line" behaviour.
"""
p.add_argument("--json", action="store_true", help="Emit only JSON on stdout.")
p.add_argument("-v", "--verbose", action="store_true", help="Verbose stderr logging.")
p.add_argument("-q", "--quiet", action="store_true", help="Suppress stderr logging.")
def _build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="metin-release",
description="Orchestration CLI for Metin2 client releases.",
)
parser.add_argument("--version", action="version", version=f"metin-release {__version__}")
parser.add_argument("--json", action="store_true", help="Emit only JSON on stdout.")
parser.add_argument("-v", "--verbose", action="store_true", help="Verbose stderr logging.")
parser.add_argument("-q", "--quiet", action="store_true", help="Suppress stderr logging.")
_add_common_flags(parser)
sub = parser.add_subparsers(dest="group", metavar="<group>")
sub.required = True
@@ -43,14 +55,18 @@ def _build_parser() -> argparse.ArgumentParser:
rsub = release.add_subparsers(dest="cmd", metavar="<command>")
rsub.required = True
inspect.add_parser(rsub)
build_manifest.add_parser(rsub)
sign.add_parser(rsub)
diff_remote.add_parser(rsub)
upload_blobs.add_parser(rsub)
promote.add_parser(rsub)
verify_public.add_parser(rsub)
publish.add_parser(rsub)
for mod in (
inspect,
build_manifest,
sign,
diff_remote,
upload_blobs,
promote,
verify_public,
publish,
):
sp = mod.add_parser(rsub)
_add_common_flags(sp)
return parser