Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# 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. 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 `<API version>+<first 6 chars of the commit hash>` — [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 (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

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.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
192 changes: 192 additions & 0 deletions scripts/refresh-openapi.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
#!/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 commits the result on a new branch.
#
# Usage:
# 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, 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
# positional argument; the argument wins if both are set)
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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

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

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
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}"

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"
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)"
TMP_README="$(mktemp)"
trap 'rm -f "$TMP_SPEC" "$TMP_README"' 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

SPEC_CHANGED=0
if diff -q "$TMP_SPEC" "$REPO_ROOT/openapi.json" >/dev/null 2>&1; then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

echo "==> openapi.json is already at $PINNED_VERSION"
else
SPEC_CHANGED=1
fi

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}**/" "$TMP_README"
fi
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},/" "$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

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 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}"

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"

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
Loading