From 99659670766a068c2b798c6979515e77580b7952 Mon Sep 17 00:00:00 2001 From: Marek Dano Date: Tue, 8 Sep 2026 11:27:52 +0100 Subject: [PATCH 1/5] docs: add release process runbook and openapi refresh scriptfix: unit test coverage Signed-off-by: Marek Dano --- RELEASE.md | 67 +++++++++++++++++++ package.json | 1 + scripts/refresh-openapi.sh | 128 +++++++++++++++++++++++++++++++++++++ 3 files changed, 196 insertions(+) create mode 100644 RELEASE.md create mode 100755 scripts/refresh-openapi.sh diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..0934e5b --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,67 @@ +# Release Process + +This repo tracks two independent version numbers: + +- **UI version** — this repo's own semver, in [`package.json`](./package.json) and tagged as `vA.B.C` on `main`. Bumped every release. +- **Pinned API version** — the upstream [IBM/mcp-context-forge](https://github.com/IBM/mcp-context-forge) commit this UI was built and tested against, recorded as build metadata on [`openapi.json`](./openapi.json)'s `info.version` (e.g. `1.0.0+589c69`). Bumped only when you refresh `openapi.json` from a newer API checkout. + +A release usually bumps both, but doesn't have to — a UI-only bugfix can ship a new UI version without touching the API pin. + +## 1. Refresh the API contract + +Skip this section if the API hasn't changed since the last release. + +```bash +npm run openapi:refresh +``` + +This runs [`scripts/refresh-openapi.sh`](./scripts/refresh-openapi.sh), which: + +1. Pulls `main` in a sibling `mcp-context-forge` checkout (default: `../mcp-context-forge`; override with `OPENAPI_SOURCE_DIR` or a path argument). +2. Regenerates `openapi.json` from it and pins `info.version` to `+` — [semver build metadata](https://semver.org/#spec-item-10), no spaces or parentheses (e.g. `1.0.0+589c69`). +3. Updates the two places the README quotes that same pin (the `This UI targets **ContextForge API vX.Y.Z**` line and the codegen note further down). +4. Runs `npm run generate` to regenerate the API client. +5. Commits on a new `chore/openapi-...` branch, pushes, and opens a PR. + +Run with `--dry-run` to stop after step 4 and inspect `git diff` yourself before committing anything. Both the API checkout and this repo must have a clean working tree before you run it. + +Fix any type errors from the client regeneration (`npm run build`) before merging the PR it opens. + +## 2. Bump the UI version + +On a branch off `main`, bump [`package.json`](./package.json)'s `version` following semver: + +- **patch** — bug fixes, no API pin change +- **minor** — new UI functionality, or an API pin bump that only adds endpoints/fields +- **major** — breaking UI change, or an API pin bump with breaking changes + +## 3. PR and merge + +Open a PR with the `openapi.json` / README / generated-client changes (if any) and the `package.json` bump. Get it reviewed and merged like any other change — no direct pushes to `main`. + +## 4. Tag the release + +```bash +git checkout main && git pull +git tag vA.B.C # must match the package.json version from step 2 +git push origin vA.B.C +``` + +## 5. Publish the GitHub release + +1. Go to the [tags page](https://github.com/contextforge-org/contextforge-web-ui/tags). +2. On the new tag's `...` menu, click **Create release**. +3. Click **Generate release notes** to populate the changelog from merged PRs. +4. If the release isn't production-ready (e.g. an early cut for testing), check **Set as a pre-release**. +5. Click **Publish release**. + +## Rolling back a bad tag + +If a tag was pushed by mistake and the release hasn't been publicised yet: + +```bash +git push --delete origin vA.B.C +git tag -d vA.B.C +``` + +Delete the corresponding GitHub release (if one was published) from its page as well. Once a release has been announced or consumed, prefer shipping a new patch version over deleting history. diff --git a/package.json b/package.json index 64e7cd4..939837c 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "i18n:extract": "formatjs extract 'src/**/*.{ts,tsx}' --out-file src/i18n/extracted.json --id-interpolation-pattern '[sha512:contenthash:base64:6]'", "i18n:compile": "formatjs compile-folder --ast src/i18n/locales src/i18n/compiled", "generate": "orval", + "openapi:refresh": "bash scripts/refresh-openapi.sh", "prepare": "husky || true" }, "dependencies": { diff --git a/scripts/refresh-openapi.sh b/scripts/refresh-openapi.sh new file mode 100755 index 0000000..323a522 --- /dev/null +++ b/scripts/refresh-openapi.sh @@ -0,0 +1,128 @@ +#!/usr/bin/env bash +# Regenerates openapi.json from a sibling mcp-context-forge checkout, pins it +# to the commit it came from, updates the README references and generated +# API client, and opens a PR with the result. +# +# Usage: +# scripts/refresh-openapi.sh [path-to-mcp-context-forge] +# scripts/refresh-openapi.sh --dry-run [path-to-mcp-context-forge] +# +# --dry-run stops after writing the local file changes so you can inspect +# `git diff` yourself; it does not commit, push, or open a PR. +# +# Env vars: +# OPENAPI_SOURCE_DIR overrides the sibling repo path (same as the +# positional argument; the argument wins if both are set) +set -euo pipefail + +DRY_RUN=0 +API_DIR_ARG="" +for arg in "$@"; do + case "$arg" in + --dry-run) DRY_RUN=1 ;; + *) API_DIR_ARG="$arg" ;; + esac +done + +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 + echo "error: $API_DIR is not a git checkout of mcp-context-forge" >&2 + echo " pass its path as an argument, or set OPENAPI_SOURCE_DIR" >&2 + exit 1 +fi + +if [[ -n "$(git -C "$REPO_ROOT" status --porcelain)" ]]; then + echo "error: $REPO_ROOT has uncommitted changes, aborting" >&2 + exit 1 +fi + +if [[ -n "$(git -C "$API_DIR" status --porcelain)" ]]; then + echo "error: $API_DIR has uncommitted changes, refusing to touch it" >&2 + exit 1 +fi + +echo "==> Updating $API_DIR" +git -C "$API_DIR" checkout main --quiet +git -C "$API_DIR" pull --ff-only --quiet + +API_COMMIT="$(git -C "$API_DIR" rev-parse HEAD)" +API_COMMIT_SHORT="${API_COMMIT:0:6}" + +VENV_PY="$API_DIR/.venv/bin/python" +if [[ -x "$VENV_PY" ]]; then + PYTHON="$VENV_PY" +else + echo "warning: no venv at $API_DIR/.venv, falling back to python3 on PATH" >&2 + PYTHON="python3" +fi + +echo "==> Generating openapi.json from $API_COMMIT_SHORT" +TMP_SPEC="$(mktemp)" +trap 'rm -f "$TMP_SPEC"' EXIT + +(cd "$API_DIR" && "$PYTHON" -c " +import json, sys +from mcpgateway.main import app +json.dump(app.openapi(), open(sys.argv[1], 'w'), indent=2) +" "$TMP_SPEC") + +API_VERSION="$(python3 -c "import json; print(json.load(open('$TMP_SPEC'))['info']['version'])")" +PINNED_VERSION="${API_VERSION}+${API_COMMIT_SHORT}" + +echo "==> Pinning info.version to $PINNED_VERSION" +python3 - "$TMP_SPEC" "$PINNED_VERSION" <<'PY' +import json, sys +path, version = sys.argv[1], sys.argv[2] +with open(path) as f: + spec = json.load(f) +spec["info"]["version"] = version +with open(path, "w") as f: + json.dump(spec, f, indent=2) + f.write("\n") +PY + +if diff -q "$TMP_SPEC" "$REPO_ROOT/openapi.json" >/dev/null 2>&1; then + echo "==> openapi.json is already at $PINNED_VERSION, nothing to do" + exit 0 +fi + +cp "$TMP_SPEC" "$REPO_ROOT/openapi.json" + +echo "==> Updating README references" +if ! grep -qE "targets \*\*ContextForge API v[0-9.]+\*\*" "$REPO_ROOT/README.md"; then + echo "warning: couldn't find the 'targets ContextForge API vX.Y.Z' line in README.md, skipping" >&2 +else + sed -i.bak -E "s/targets \*\*ContextForge API v[0-9.]+\*\*/targets **ContextForge API v${API_VERSION}**/" "$REPO_ROOT/README.md" +fi +if ! grep -qE "pinned to API v[0-9.]+," "$REPO_ROOT/README.md"; then + echo "warning: couldn't find the 'pinned to API vX.Y.Z,' line in README.md, skipping" >&2 +else + sed -i.bak -E "s/pinned to API v[0-9.]+,/pinned to API v${API_VERSION},/" "$REPO_ROOT/README.md" +fi +rm -f "$REPO_ROOT/README.md.bak" + +echo "==> Regenerating API client" +(cd "$REPO_ROOT" && npm run generate) + +if [[ "$DRY_RUN" == "1" ]]; then + echo "==> --dry-run set: left openapi.json, README.md and src/generated updated locally." + echo " Review with 'git diff' and commit/branch/PR yourself when ready." + exit 0 +fi + +BRANCH="chore/openapi-${API_VERSION}-${API_COMMIT_SHORT}" +echo "==> Creating branch $BRANCH" +git -C "$REPO_ROOT" checkout -b "$BRANCH" +git -C "$REPO_ROOT" add openapi.json README.md src/generated +git -C "$REPO_ROOT" commit -m "chore: refresh openapi.json to API v${PINNED_VERSION} + +Co-Authored-By: Claude Sonnet 5 " + +echo "==> Pushing and opening PR" +git -C "$REPO_ROOT" push -u origin "$BRANCH" +gh pr create \ + --repo contextforge-org/contextforge-web-ui \ + --title "chore: refresh openapi.json to API v${PINNED_VERSION}" \ + --body "Regenerated from [IBM/mcp-context-forge@${API_COMMIT_SHORT}](https://github.com/IBM/mcp-context-forge/commit/${API_COMMIT})." From e60ae674bee8f405d4eed6982d89bf52df9209ac Mon Sep 17 00:00:00 2001 From: Marek Dano Date: Tue, 8 Sep 2026 11:34:42 +0100 Subject: [PATCH 2/5] fix: release script Signed-off-by: Marek Dano --- scripts/refresh-openapi.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/refresh-openapi.sh b/scripts/refresh-openapi.sh index 323a522..d716456 100755 --- a/scripts/refresh-openapi.sh +++ b/scripts/refresh-openapi.sh @@ -115,7 +115,7 @@ fi BRANCH="chore/openapi-${API_VERSION}-${API_COMMIT_SHORT}" echo "==> Creating branch $BRANCH" git -C "$REPO_ROOT" checkout -b "$BRANCH" -git -C "$REPO_ROOT" add openapi.json README.md src/generated +git -C "$REPO_ROOT" add openapi.json README.md git -C "$REPO_ROOT" commit -m "chore: refresh openapi.json to API v${PINNED_VERSION} Co-Authored-By: Claude Sonnet 5 " From 45b353f1576b652012582b8ee7a2e310fd0b7603 Mon Sep 17 00:00:00 2001 From: Marek Dano Date: Tue, 8 Sep 2026 11:37:47 +0100 Subject: [PATCH 3/5] fix: release script Signed-off-by: Marek Dano --- scripts/refresh-openapi.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/refresh-openapi.sh b/scripts/refresh-openapi.sh index d716456..e09a04d 100755 --- a/scripts/refresh-openapi.sh +++ b/scripts/refresh-openapi.sh @@ -114,7 +114,7 @@ fi BRANCH="chore/openapi-${API_VERSION}-${API_COMMIT_SHORT}" echo "==> Creating branch $BRANCH" -git -C "$REPO_ROOT" checkout -b "$BRANCH" +git -C "$REPO_ROOT" checkout -B "$BRANCH" git -C "$REPO_ROOT" add openapi.json README.md git -C "$REPO_ROOT" commit -m "chore: refresh openapi.json to API v${PINNED_VERSION} From 992454af8ea286607e2f87b91952b64524bec3f4 Mon Sep 17 00:00:00 2001 From: Marek Dano Date: Tue, 8 Sep 2026 11:47:58 +0100 Subject: [PATCH 4/5] fix: release script with signoff flag Signed-off-by: Marek Dano --- scripts/refresh-openapi.sh | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/scripts/refresh-openapi.sh b/scripts/refresh-openapi.sh index e09a04d..e33df47 100755 --- a/scripts/refresh-openapi.sh +++ b/scripts/refresh-openapi.sh @@ -116,9 +116,7 @@ BRANCH="chore/openapi-${API_VERSION}-${API_COMMIT_SHORT}" echo "==> Creating branch $BRANCH" git -C "$REPO_ROOT" checkout -B "$BRANCH" git -C "$REPO_ROOT" add openapi.json README.md -git -C "$REPO_ROOT" commit -m "chore: refresh openapi.json to API v${PINNED_VERSION} - -Co-Authored-By: Claude Sonnet 5 " +git -C "$REPO_ROOT" commit --signoff -m "chore: refresh openapi.json to API v${PINNED_VERSION}" echo "==> Pushing and opening PR" git -C "$REPO_ROOT" push -u origin "$BRANCH" From a466fcb9dcf388122052e91b8576804c2ffb13e5 Mon Sep 17 00:00:00 2001 From: Marek Dano Date: Tue, 8 Sep 2026 11:48:33 +0100 Subject: [PATCH 5/5] chore: refresh openapi.json to API v1.0.10+13d549 Signed-off-by: Marek Dano --- README.md | 4 ++-- openapi.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0142f9f..b92bf1f 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ This repository holds the BFF and client that sit in front of it. The table belo Throughout this README, "the API", "the BFF", and "the client" refer to those three. The browser only ever talks to the BFF, never directly to the API. -This UI targets **ContextForge API v1.0.7**, matching [`openapi.json`](./openapi.json) committed at repo root. +This UI targets **ContextForge API v1.0.10**, matching [`openapi.json`](./openapi.json) committed at repo root. ## Tech Stack @@ -138,7 +138,7 @@ npm run preview ## API Types -TypeScript types and fetch clients under `src/generated/` come from [`openapi.json`](./openapi.json) via [Orval](./orval.config.ts). That file is committed and pinned to API v1.0.7, not re-fetched at build time. +TypeScript types and fetch clients under `src/generated/` come from [`openapi.json`](./openapi.json) via [Orval](./orval.config.ts). That file is committed and pinned to API v1.0.10, not re-fetched at build time. ```bash npm run generate # regenerate src/generated/ from ./openapi.json diff --git a/openapi.json b/openapi.json index 8a6de87..b7d589c 100644 --- a/openapi.json +++ b/openapi.json @@ -3,7 +3,7 @@ "info": { "title": "ContextForge", "description": "ContextForge AI Gateway \u2014 an AI gateway, registry, and proxy for MCP, A2A, and REST/gRPC APIs. Exposes a unified control plane with centralized governance, discovery, and observability. Optimizes agent and tool calling, and supports plugins.", - "version": "1.0.9+ (ref: 589c69)" + "version": "1.0.10+13d549" }, "paths": { "/metrics/prometheus": { @@ -10454,7 +10454,7 @@ "Teams" ], "summary": "Create Team", - "description": "Create a new team, optionally seeding it with members.\n\nMembers supplied in the request are routed by the server: an address that\nbelongs to an active user is added to the team directly, anything else is\nsent an invitation. Team, memberships and invitations are written as one\ntransaction, so a bad row fails the whole request rather than leaving a\nhalf-populated team behind.\n\nArgs:\n request: Team creation request data\n background_tasks: Response-scoped background task scheduler\n current_user_ctx: Currently authenticated user context\n db: Database session\n\nReturns:\n TeamCreateResponse: Created team data, plus how each seeded member was resolved\n\nRaises:\n HTTPException: If team creation fails\n\nExamples:\n >>> import asyncio\n >>> asyncio.iscoroutinefunction(create_team)\n True", + "description": "Create a new team, optionally seeding it with members.\n\nMembers supplied in the request are routed by the server: an address that\nbelongs to an active user is added to the team directly, anything else is\nsent an invitation. Team, memberships and invitations are written as one\ntransaction, so a bad row fails the whole request rather than leaving a\nhalf-populated team behind.\n\nArgs:\n request: Team creation request data\n background_tasks: Response-scoped background task scheduler\n current_user_ctx: Currently authenticated user context\n db: Database session\n\nReturns:\n TeamCreateResponse: Created team data, plus how each seeded member was resolved\n\nRaises:\n HTTPException 400: Active team with same name exists (platform admin callers).\n HTTPException 403: Team creation disabled; caller is not platform admin.\n HTTPException 409: Active team with same name exists (non-admin; generic message prevents name enumeration).\n HTTPException 500: Unexpected service error.\n\nExamples:\n >>> import asyncio\n >>> asyncio.iscoroutinefunction(create_team)\n True", "operationId": "create_team_v1_teams__post", "security": [ {