-
Notifications
You must be signed in to change notification settings - Fork 2
docs: add release process runbook and openapi refresh automation #112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
marekdano
wants to merge
5
commits into
main
Choose a base branch
from
606-release-docs
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+264
−0
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
9965967
docs: add release process runbook and openapi refresh scriptfix: unit…
marekdano e60ae67
fix: release script
marekdano 45b353f
fix: release script
marekdano 992454a
fix: release script with signoff flag
marekdano d09afc0
fix: address review feedback on release script
marekdano File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| 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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.