From 385efde137dac00fea4f03eabcc827291824a1f3 Mon Sep 17 00:00:00 2001 From: McAmner Date: Sat, 18 Jul 2026 19:10:50 +0200 Subject: [PATCH] feat(release): release-check.sh conforms to repo_release_check.v1 Adapt the existing root release-check.sh to the multi-repo preflight contract: --json emits a repo_release_check.v1 object on clean stdout (checks routed to stderr), exit 0; --json/--dry-run skip the clean-git-tree check (preflight owns it). Human mode unchanged. Existing checks preserved (VERSION/pyproject/ CHANGELOG surfaces, docs presence, pytest, CLI, compileall, MCP payloads, no weights/.env). Verified: shellcheck clean; --dry-run --json emits valid READY JSON against the real venv; human --dry-run exits 0. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 9 ++- release-check.sh | 157 +++++++++++++++++++++++++---------------------- 2 files changed, 92 insertions(+), 74 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 584269b..38d66ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,14 @@ ## Unreleased -No unreleased changes. +### Changed + +- `release-check.sh` now conforms to the `repo_release_check.v1` contract: + `--json` emits the machine-readable verdict (`schema`, `repo`, `status`, + `blockers`, `warnings`, `evidence`) on clean stdout and exits 0; `--dry-run` + and `--json` skip the clean-tree requirement (the caller owns it). Human mode + is unchanged. Lets mq-agent's `stack release --all --preflight` read the + release verdict. ## 1.4.0 — 2026-06-03 diff --git a/release-check.sh b/release-check.sh index 04755e9..a147f40 100755 --- a/release-check.sh +++ b/release-check.sh @@ -1,7 +1,27 @@ #!/usr/bin/env bash -set -euo pipefail +# Release readiness check for mq-image-analyze. Read-only. +# +# Human mode (no flags / --dry-run): prints per-check ok/FAIL, exits 1 on any +# failure. --dry-run skips the clean-git-tree requirement. +# Contract mode (--json): emits a repo_release_check.v1 object on stdout and +# exits 0 (the `status` field carries the verdict). Consumed by mq-agent's +# `stack release --all --preflight`. --json implies --dry-run (the preflight +# owns the dirty-tree check). +set -uo pipefail REPO_ROOT="$(cd "$(dirname "$0")" && pwd)" +cd "$REPO_ROOT" || exit 1 + +DRY_RUN=0 +JSON=0 +for arg in "$@"; do + case "$arg" in + --dry-run) DRY_RUN=1 ;; + --json) JSON=1 ;; + *) echo "usage: ./release-check.sh [--dry-run] [--json]" >&2; exit 2 ;; + esac +done +[[ "$JSON" -eq 1 ]] && DRY_RUN=1 # Prefer venv binaries if available PYTHON="${REPO_ROOT}/.venv/bin/python" @@ -9,61 +29,67 @@ MQ_IMAGE="${REPO_ROOT}/.venv/bin/mq-image" [[ -x "$PYTHON" ]] || PYTHON="python" [[ -x "$MQ_IMAGE" ]] || MQ_IMAGE="mq-image" -ERRORS=0 - -# Handles fail. -fail() { echo "FAIL: $1"; ERRORS=$((ERRORS + 1)); } -# Handles ok. -ok() { echo " ok: $1"; } +BLOCKERS=() +say() { [[ "$JSON" -eq 1 ]] || echo "$1"; } +ok() { [[ "$JSON" -eq 1 ]] || echo " ok: $1"; } +fail() { BLOCKERS+=("$1"); [[ "$JSON" -eq 1 ]] || echo "FAIL: $1" >&2; } + +# run LABEL CMD... — record a blocker on failure; keep stdout clean by routing +# captured output to stderr (human mode) only on failure. +run() { + local label="$1"; shift + local out + if out="$("$@" 2>&1)"; then + ok "$label" + else + fail "$label" + [[ "$JSON" -eq 1 ]] || printf '%s\n' "$out" >&2 + fi +} -echo "==> Release check" -echo "" +say "==> Release check" -# Git state -if [[ -n "$(git status --porcelain)" ]]; then - fail "Uncommitted changes present" -else - ok "Git working tree clean" +# Git state (skipped in --dry-run/preflight — the caller owns the dirty check) +if [[ "$DRY_RUN" -eq 0 ]]; then + if [[ -n "$(git status --porcelain)" ]]; then + fail "Uncommitted changes present" + else + ok "Git working tree clean" + fi fi -# VERSION +VERSION="" if [[ ! -f VERSION ]]; then fail "VERSION file missing" else - VERSION=$(cat VERSION) + VERSION="$(cat VERSION)" ok "VERSION = $VERSION" fi -# CHANGELOG mentions current version -if ! grep -q "$VERSION" CHANGELOG.md 2>/dev/null; then +if [[ -n "$VERSION" ]] && ! grep -q "$VERSION" CHANGELOG.md 2>/dev/null; then fail "CHANGELOG.md does not mention version $VERSION" -else +elif [[ -n "$VERSION" ]]; then ok "CHANGELOG.md mentions $VERSION" fi -# pyproject version matches VERSION -PYPROJECT_VERSION=$("$PYTHON" - <<'PY' +PYPROJECT_VERSION="$("$PYTHON" - <<'PY' 2>/dev/null import tomllib from pathlib import Path - -data = tomllib.loads(Path("pyproject.toml").read_text()) -print(data["project"]["version"]) +print(tomllib.loads(Path("pyproject.toml").read_text())["project"]["version"]) PY -) +)" if [[ "$PYPROJECT_VERSION" != "$VERSION" ]]; then - fail "pyproject.toml version $PYPROJECT_VERSION does not match VERSION $VERSION" + fail "pyproject.toml version '$PYPROJECT_VERSION' != VERSION '$VERSION'" else ok "pyproject.toml version matches VERSION" fi -# README exists and is non-empty if [[ ! -s README.md ]]; then fail "README.md missing or empty" else ok "README.md present" fi -# Docs for doc in docs/architecture.md docs/cli.md docs/json-schema.md docs/mcp-tools.md docs/tool-safety.md; do if [[ ! -f "$doc" ]]; then fail "$doc missing" @@ -72,68 +98,53 @@ for doc in docs/architecture.md docs/cli.md docs/json-schema.md docs/mcp-tools.m fi done -# Tests pass -echo "" -echo "==> Running tests" -if "$PYTHON" -m pytest -q 2>&1; then - ok "Tests pass" -else - fail "Tests failed" -fi - -# CLI works -echo "" -echo "==> CLI checks" -if "$MQ_IMAGE" --help > /dev/null 2>&1; then - ok "mq-image --help" -else - fail "mq-image --help failed" -fi - -if "$MQ_IMAGE" --version > /dev/null 2>&1; then - ok "mq-image --version" -else - fail "mq-image --version failed" -fi +say "==> Running tests" +run "pytest" "$PYTHON" -m pytest -q -if "$MQ_IMAGE" mcp --help > /dev/null 2>&1; then - ok "mq-image mcp --help" -else - fail "mq-image mcp --help failed" -fi +say "==> CLI checks" +run "mq-image --help" "$MQ_IMAGE" --help +run "mq-image --version" "$MQ_IMAGE" --version +run "mq-image mcp --help" "$MQ_IMAGE" mcp --help -# Compileall -if "$PYTHON" -m compileall -q mq_image_analyze 2>&1; then - ok "compileall mq_image_analyze" -else - fail "compileall failed" -fi - -# MCP sample payload freshness -if "$PYTHON" scripts/check-mcp-sample-payloads.py 2>&1; then - ok "MCP sample payloads match live tool contracts" -else - fail "MCP sample payload validation failed" -fi +run "compileall mq_image_analyze" "$PYTHON" -m compileall -q mq_image_analyze +run "MCP sample payloads match live tool contracts" "$PYTHON" scripts/check-mcp-sample-payloads.py -# No model weights committed if git ls-files | grep -qE '\.(pt|ckpt|safetensors|bin|gguf|onnx)$'; then fail "Model weight files are tracked in git" else ok "No model weights in git" fi -# No .env committed if git ls-files | grep -q '\.env'; then fail ".env file tracked in git" else ok "No .env in git" fi +if [[ "$JSON" -eq 1 ]]; then + status=READY + [[ "${#BLOCKERS[@]}" -gt 0 ]] && status=BLOCKED + "$PYTHON" - "$status" "$VERSION" ${BLOCKERS[@]+"${BLOCKERS[@]}"} <<'PY' +import json +import sys + +status, version, *blockers = sys.argv[1:] +print(json.dumps({ + "schema": "repo_release_check.v1", + "repo": "mq-image-analyze", + "status": status, + "blockers": blockers, + "warnings": [], + "evidence": {"version": version}, +})) +PY + exit 0 +fi + echo "" -if [[ $ERRORS -eq 0 ]]; then +if [[ "${#BLOCKERS[@]}" -eq 0 ]]; then echo "Release check passed. Ready to tag v$VERSION." else - echo "$ERRORS check(s) failed. Fix before releasing." + echo "${#BLOCKERS[@]} check(s) failed. Fix before releasing." exit 1 fi