docs: add release process runbook and openapi refresh automation - #112
docs: add release process runbook and openapi refresh automation#112marekdano wants to merge 6 commits into
Conversation
… test coverage Signed-off-by: Marek Dano <mk.dano@gmail.com>
Signed-off-by: Marek Dano <mk.dano@gmail.com>
Signed-off-by: Marek Dano <mk.dano@gmail.com>
Signed-off-by: Marek Dano <mk.dano@gmail.com>
gcgoncalves
left a comment
There was a problem hiding this comment.
The RELEASE.md instructions are clear and simple, I like it! :)
A few considerations about the script:
- The script leans on
gh, supposing the user has it installed on the first place. Can we replace it withgit? - The script ends pushing the changes by default. Do we actually want that?
When running the script locally, I got the following error:
==> Updating ~/Projects/mcp-context-forge
==> Generating openapi.json from 63d69a
2026-09-08T13:23:22 - mcpgateway.config - INFO - Using SQLite database. Consider PostgreSQL for production.
2026-09-08T13:23:22 - mcpgateway.config - INFO - SIEM URL allowlist is empty — all outbound destination URLs are permitted
Traceback (most recent call last):
...
File "~/Projects/mcp-context-forge/mcpgateway/config.py", line 1671, in _enforce_secret_strength
raise SecurityConfigurationError(f"{field_name}: unset placeholder (__REPLACE_ME__) rejected. {remediation}{hint}")
mcpgateway.config.SecurityConfigurationError: jwt_secret_key: unset placeholder (__REPLACE_ME__) rejected. Run 'python -m mcpgateway.scripts.init_secrets' to generate strong values, or use 'make init-secrets-patch-env' to write them directly into .env.
ELIFECYCLE Command failed with exit code 1.This means that .env has no JWT secret set. I think we need to document this and, ideally, handle this error on the script and instruct the user on how to proceed.
| REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" | ||
| API_DIR="${API_DIR_ARG:-${OPENAPI_SOURCE_DIR:-$(dirname "$REPO_ROOT")/mcp-context-forge}}" | ||
|
|
||
| if [[ ! -d "$API_DIR/.git" ]]; then |
There was a problem hiding this comment.
Thanks for adding the dirty-tree guard. Could we also verify this checkout has the canonical IBM/mcp-context-forge remote before checking it out, pulling it, and importing its Python app? Right now any local .git path supplied through the argument or environment variable is trusted and its code runs in the release operator environment.
| f.write("\n") | ||
| PY | ||
|
|
||
| if diff -q "$TMP_SPEC" "$REPO_ROOT/openapi.json" >/dev/null 2>&1; then |
There was a problem hiding this comment.
Nice idempotency check. Could README synchronization happen before this early return, or be evaluated separately? If openapi.json already matches while its README version references are stale, the command reports nothing to do and skips the advertised README update.
|
|
||
| BRANCH="chore/openapi-${API_VERSION}-${API_COMMIT_SHORT}" | ||
| echo "==> Creating branch $BRANCH" | ||
| git -C "$REPO_ROOT" checkout -B "$BRANCH" |
There was a problem hiding this comment.
Could we base this branch explicitly on origin/main? checkout -B uses whichever commit the caller currently has checked out; running from a clean feature or stale branch would make the generated PR include unrelated commits.
- Verify the sibling checkout's remote points at IBM/mcp-context-forge before pulling and importing its Python app - Check README references for staleness independently of the openapi.json idempotency check, so a stale README alone still updates - Base the new branch explicitly on origin/main instead of whatever is currently checked out - Require --push to push/open a PR instead of doing it by default, with gh now optional - Fail fast with a clear message when the sibling repo's .env still has unset secret placeholders Signed-off-by: Marek Dano <mk.dano@gmail.com>
| PY | ||
|
|
||
| SPEC_CHANGED=0 | ||
| if diff -q "$TMP_SPEC" "$REPO_ROOT/openapi.json" >/dev/null 2>&1; then |
There was a problem hiding this comment.
Thanks for anchoring the created branch to origin/main. Could change detection also use that same target? These flags are calculated before the fetch/checkout. If a clean caller branch already contains the refreshed spec but origin/main does not, SPEC_CHANGED is zero; the new branch starts from stale origin/main and never copies the refreshed spec.
| while read -r remote_name; do | ||
| [[ -z "$remote_name" ]] && continue | ||
| remote_url="$(git -C "$API_DIR" remote get-url "$remote_name" 2>/dev/null || true)" | ||
| if [[ "$remote_url" =~ [:/][Ii][Bb][Mm]/mcp-context-forge(\.git)?$ ]]; then |
There was a problem hiding this comment.
Appreciate the new remote check. Could we require the exact canonical GitHub URL and retain that remote name for the fetch? This pattern also accepts an attacker host such as https://evil.example/IBM/mcp-context-forge.git, and validating any remote does not ensure main pulls from that remote before its Python code is imported.
| README_CHANGED=1 | ||
| fi | ||
|
|
||
| if [[ "$SPEC_CHANGED" == 0 && "$README_CHANGED" == 0 ]]; then |
There was a problem hiding this comment.
Small flow issue: after a successful local-only run, re-running with --push finds no spec/README changes and returns here before reaching the push block. The documented re-run with --push path therefore cannot push the branch. Please handle --push for the existing generated branch, or adjust the guidance.
vishu-bh
left a comment
There was a problem hiding this comment.
Thanks for addressing prior feedback — branch anchoring, README checks, secret guidance, and opt-in push behavior are all stronger. I found three remaining release-flow/security issues in the new inline comments. Please address them before merge.
a-effort
left a comment
There was a problem hiding this comment.
Reviewed the diff. One blocker.
The refresh-openapi.sh script generates the spec by importing and calling app.openapi() from mcpgateway.main directly in Python. This means the script executes arbitrary code from the sibling checkout — including all startup side effects of that module. The script guards against running from an unverified remote, which is good, but the guard checks only remote URL pattern by string match, not that the working tree matches the remote. A checkout with a valid remote URL but local modifications (hotfixes, experiments) would pass the guard and execute modified code. The clean working tree check for the API dir catches this: [[ -n "$(git -C "$API_DIR" status --porcelain)" ]] aborts if there are uncommitted changes. That is an adequate guard, but it does not catch the case where the working tree is clean but is on a non-main branch that has diverged from the remote. The script calls git checkout main --quiet before pulling, which addresses this, but only if there are no local main branch commits that haven't been pushed. If origin/main and local main have diverged, pull --ff-only will fail with an error, which is the correct behavior. No change needed; noting this so reviewers understand the trust model.
Separate issue: the script writes openapi.json using Python's json.dump with indent=2 but does not set ensure_ascii=False. If the spec contains non-ASCII characters (unlikely but possible in description fields), they will be escaped to \uXXXX sequences in the output, which would produce a diff against any version that had those characters unescaped. This is a cosmetic issue but could produce spurious noise in openapi.json diffs. Consider adding ensure_ascii=False to both json.dump calls.
Summary
RELEASE.md, documenting the end-to-end release process: refreshing the pinned API contract, bumping the UI version, taggingmain, and publishing the GitHub release.scripts/refresh-openapi.sh(wired up asnpm run openapi:refresh), automating the "regenerate openapi.json from a sibling mcp-context-forge checkout" step: pulls upstreammain, regenerates the spec, pinsinfo.versionto<API version>+<commit hash>, updates the two README references to that version, regenerates the API client, and opens a signed-off PR with the result. Supports--dry-runto inspect the diff before anything is committed.No application code changes — docs and release tooling only.