diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..44484b8 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,141 @@ +name: Release + +on: + push: + tags: + - "v*" + workflow_dispatch: + inputs: + tag: + description: "Existing v* tag to publish as a GitHub Release and to the MCP Registry" + required: true + type: string + +permissions: + contents: write + +jobs: + github-release: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Publish GitHub Release + env: + GH_TOKEN: ${{ github.token }} + EVENT_NAME: ${{ github.event_name }} + INPUT_TAG: ${{ github.event.inputs.tag }} + run: | + set -euo pipefail + + tag="${GITHUB_REF_NAME}" + if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then + tag="$INPUT_TAG" + fi + + if [[ ! "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then + echo "Refusing to publish non-semver tag: $tag" >&2 + exit 1 + fi + + git fetch --tags --force + if ! git rev-parse -q --verify "refs/tags/$tag" >/dev/null; then + echo "Tag does not exist on the remote: $tag" >&2 + exit 1 + fi + + if gh release view "$tag" >/dev/null 2>&1; then + echo "GitHub Release already exists for $tag; nothing to do." + exit 0 + fi + + args=(--verify-tag --generate-notes --title "Release $tag") + if [[ "$tag" == *-* ]]; then + args+=(--prerelease --latest=false) + else + args+=(--latest) + fi + + gh release create "$tag" "${args[@]}" + + publish-registry: + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + id-token: write + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 24 + + - name: Resolve tag + id: tag + env: + EVENT_NAME: ${{ github.event_name }} + INPUT_TAG: ${{ github.event.inputs.tag }} + run: | + set -euo pipefail + + tag="${GITHUB_REF_NAME}" + if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then + tag="$INPUT_TAG" + fi + + if [[ ! "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then + echo "Refusing to publish non-semver tag: $tag" >&2 + exit 1 + fi + + echo "version=${tag#v}" >>"$GITHUB_OUTPUT" + + - name: Verify server.json matches tag + env: + VERSION: ${{ steps.tag.outputs.version }} + run: | + set -euo pipefail + + server_version="$(node -p "require('./server.json').version")" + package_version="$(node -p "require('./server.json').packages[0].version")" + if [[ "$server_version" != "$VERSION" || "$package_version" != "$VERSION" ]]; then + echo "server.json ($server_version / package $package_version) does not match tag v$VERSION" >&2 + exit 1 + fi + + - name: Wait for npm package + env: + VERSION: ${{ steps.tag.outputs.version }} + run: | + set -euo pipefail + + for attempt in $(seq 1 20); do + if npm view "portkey-admin-mcp@${VERSION}" version >/dev/null 2>&1; then + echo "Found portkey-admin-mcp@${VERSION} on npm" + exit 0 + fi + echo "Waiting for portkey-admin-mcp@${VERSION} on npm (attempt ${attempt}/20)..." + sleep 15 + done + + echo "Timed out waiting for portkey-admin-mcp@${VERSION} on npm" >&2 + exit 1 + + - name: Install mcp-publisher + run: | + set -euo pipefail + curl -fsSL "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_linux_amd64.tar.gz" \ + | tar -xz mcp-publisher + + - name: Authenticate with the MCP Registry + run: ./mcp-publisher login github-oidc + + - name: Publish to the MCP Registry + run: ./mcp-publisher publish diff --git a/.gitignore b/.gitignore index 20b6de8..8250a1c 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,11 @@ dist/ *.swp *.swo +# Agent/tooling artifacts +.claude/ +.playwright-mcp/ +.vercel/ + # OS specific .DS_Store Thumbs.db diff --git a/docs/RELEASE.md b/docs/RELEASE.md new file mode 100644 index 0000000..6bc6864 --- /dev/null +++ b/docs/RELEASE.md @@ -0,0 +1,41 @@ +# Release Process + +This project uses stable SemVer tags plus GitHub Releases so package registries +and catalog scanners can detect published versions. + +## Publish a New Stable Release + +1. Update `package.json`, `package-lock.json`, `server.json`, and + `CHANGELOG.md` for the new version. Keep `server.json`'s top-level + `version` and `packages[0].version` in sync with `package.json`. +2. Run `npm run ci`. +3. Commit the release changes. +4. Publish the npm package: + + ```bash + npm publish --access public + ``` + +5. Create and push a stable tag: + + ```bash + git tag v0.4.0 + git push origin v0.4.0 + ``` + +The `Release` workflow runs two jobs on every pushed `v*` tag: + +- **`github-release`** publishes a non-prerelease GitHub Release for stable + tags like `v0.4.0`. Tags containing a hyphen, such as `v0.4.0-beta.1`, are + published as prereleases and are not marked as the latest release. +- **`publish-registry`** publishes `server.json` to the + [MCP Registry](https://registry.modelcontextprotocol.io). It authenticates + via GitHub Actions OIDC (no secrets required), verifies `server.json` + matches the tag, and waits for the matching npm version to be available + before publishing. Publishing npm before pushing the tag (step 4 then 5) + avoids the wait. + +## Backfill an Existing Tag + +Use the `Release` workflow's manual dispatch and pass the existing tag name. +The workflow verifies that the tag exists before publishing a GitHub Release.