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 d09afc0badb1c7ea4f92d7d27618dd172769a673 Mon Sep 17 00:00:00 2001 From: Marek Dano Date: Tue, 8 Sep 2026 14:27:21 +0100 Subject: [PATCH 5/5] fix: address review feedback on release script - 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 --- RELEASE.md | 8 ++- scripts/refresh-openapi.sh | 114 +++++++++++++++++++++++++++++-------- 2 files changed, 96 insertions(+), 26 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index 0934e5b..30f499b 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -17,14 +17,18 @@ 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). +1. Verifies the sibling checkout has a remote pointing at `IBM/mcp-context-forge`, then pulls `main` there (default location: `../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. +5. Commits on a new `chore/openapi-...` branch (based on `origin/main`). 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. +By default the script stops after committing locally — push the branch and open the PR yourself, or re-run with `--push` to have it push and open the PR for you (via `gh` if installed; otherwise it prints a compare URL). + +The sibling checkout needs real secrets in its `.env` (not the `__REPLACE_ME__` placeholders) to boot the app and produce the spec — run `python -m mcpgateway.scripts.init_secrets` or `make init-secrets-patch-env` there first if you haven't. + Fix any type errors from the client regeneration (`npm run build`) before merging the PR it opens. ## 2. Bump the UI version diff --git a/scripts/refresh-openapi.sh b/scripts/refresh-openapi.sh index e33df47..0d6d4b1 100755 --- a/scripts/refresh-openapi.sh +++ b/scripts/refresh-openapi.sh @@ -1,14 +1,17 @@ #!/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. +# API client, and commits the result on a new branch. # # Usage: -# scripts/refresh-openapi.sh [path-to-mcp-context-forge] -# scripts/refresh-openapi.sh --dry-run [path-to-mcp-context-forge] +# scripts/refresh-openapi.sh [--dry-run] [--push] [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. +# `git diff` yourself; it does not commit, branch, or push. +# --push pushes the commit and opens a PR (via `gh`, if installed; +# otherwise it prints the branch name and a compare URL so you can +# open the PR by hand). Without this flag the script only commits +# locally and leaves pushing/PR creation to you. # # Env vars: # OPENAPI_SOURCE_DIR overrides the sibling repo path (same as the @@ -16,10 +19,12 @@ set -euo pipefail DRY_RUN=0 +PUSH=0 API_DIR_ARG="" for arg in "$@"; do case "$arg" in --dry-run) DRY_RUN=1 ;; + --push) PUSH=1 ;; *) API_DIR_ARG="$arg" ;; esac done @@ -33,6 +38,25 @@ if [[ ! -d "$API_DIR/.git" ]]; then exit 1 fi +REMOTE_OK=0 +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 + REMOTE_OK=1 + break + fi +done < <(git -C "$API_DIR" remote) + +if [[ "$REMOTE_OK" != 1 ]]; then + echo "error: $API_DIR has no remote pointing at IBM/mcp-context-forge" >&2 + echo " refusing to pull and execute code from an unverified checkout." >&2 + echo " point OPENAPI_SOURCE_DIR/the path argument at a checkout of the canonical repo," >&2 + echo " or add it as a remote, e.g.:" >&2 + echo " git -C \"$API_DIR\" remote add upstream https://github.com/IBM/mcp-context-forge.git" >&2 + exit 1 +fi + if [[ -n "$(git -C "$REPO_ROOT" status --porcelain)" ]]; then echo "error: $REPO_ROOT has uncommitted changes, aborting" >&2 exit 1 @@ -50,6 +74,13 @@ git -C "$API_DIR" pull --ff-only --quiet API_COMMIT="$(git -C "$API_DIR" rev-parse HEAD)" API_COMMIT_SHORT="${API_COMMIT:0:6}" +if [[ -f "$API_DIR/.env" ]] && grep -q '__REPLACE_ME__' "$API_DIR/.env"; then + echo "error: $API_DIR/.env still has unset __REPLACE_ME__ secret placeholders." >&2 + echo " Run 'python -m mcpgateway.scripts.init_secrets' in that checkout" >&2 + echo " (or 'make init-secrets-patch-env' to write them into .env), then re-run this script." >&2 + exit 1 +fi + VENV_PY="$API_DIR/.venv/bin/python" if [[ -x "$VENV_PY" ]]; then PYTHON="$VENV_PY" @@ -60,7 +91,8 @@ fi echo "==> Generating openapi.json from $API_COMMIT_SHORT" TMP_SPEC="$(mktemp)" -trap 'rm -f "$TMP_SPEC"' EXIT +TMP_README="$(mktemp)" +trap 'rm -f "$TMP_SPEC" "$TMP_README"' EXIT (cd "$API_DIR" && "$PYTHON" -c " import json, sys @@ -83,44 +115,78 @@ with open(path, "w") as f: f.write("\n") PY +SPEC_CHANGED=0 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 + echo "==> openapi.json is already at $PINNED_VERSION" +else + SPEC_CHANGED=1 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 "==> Checking README references" +cp "$REPO_ROOT/README.md" "$TMP_README" +if ! grep -qE "targets \*\*ContextForge API v[0-9.]+\*\*" "$TMP_README"; 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" + sed -i.bak -E "s/targets \*\*ContextForge API v[0-9.]+\*\*/targets **ContextForge API v${API_VERSION}**/" "$TMP_README" fi -if ! grep -qE "pinned to API v[0-9.]+," "$REPO_ROOT/README.md"; then +if ! grep -qE "pinned to API v[0-9.]+," "$TMP_README"; 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" + sed -i.bak -E "s/pinned to API v[0-9.]+,/pinned to API v${API_VERSION},/" "$TMP_README" +fi +rm -f "$TMP_README.bak" + +README_CHANGED=0 +if ! diff -q "$TMP_README" "$REPO_ROOT/README.md" >/dev/null 2>&1; then + README_CHANGED=1 fi -rm -f "$REPO_ROOT/README.md.bak" -echo "==> Regenerating API client" -(cd "$REPO_ROOT" && npm run generate) +if [[ "$SPEC_CHANGED" == 0 && "$README_CHANGED" == 0 ]]; then + echo "==> Nothing to update, already at $PINNED_VERSION" + exit 0 +fi + +# Write the generated files into the working tree. Deferred until here (and, +# for a real run, until after switching to the release branch below) so that +# switching branches never has to reconcile uncommitted local edits against +# origin/main's version of these files. +write_changes() { + [[ "$SPEC_CHANGED" == 1 ]] && cp "$TMP_SPEC" "$REPO_ROOT/openapi.json" + [[ "$README_CHANGED" == 1 ]] && cp "$TMP_README" "$REPO_ROOT/README.md" + echo "==> Regenerating API client" + (cd "$REPO_ROOT" && npm run generate) +} if [[ "$DRY_RUN" == "1" ]]; then + write_changes 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 +git -C "$REPO_ROOT" fetch origin main --quiet BRANCH="chore/openapi-${API_VERSION}-${API_COMMIT_SHORT}" -echo "==> Creating branch $BRANCH" -git -C "$REPO_ROOT" checkout -B "$BRANCH" +echo "==> Creating branch $BRANCH from origin/main" +git -C "$REPO_ROOT" checkout -B "$BRANCH" origin/main --quiet +write_changes git -C "$REPO_ROOT" add openapi.json README.md git -C "$REPO_ROOT" commit --signoff -m "chore: refresh openapi.json to API v${PINNED_VERSION}" -echo "==> Pushing and opening PR" +if [[ "$PUSH" != "1" ]]; then + echo "==> Committed on $BRANCH. Re-run with --push to push and open a PR," + echo " or push/open it yourself when you're ready." + exit 0 +fi + +echo "==> Pushing $BRANCH" 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})." + +if command -v gh >/dev/null 2>&1; then + 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})." +else + echo "==> gh not found; open a PR for $BRANCH yourself:" + echo " https://github.com/contextforge-org/contextforge-web-ui/compare/main...${BRANCH}?expand=1" +fi