Skip to content

ref(mcp): drop the root .mcp.json compat copy (#319) #70

ref(mcp): drop the root .mcp.json compat copy (#319)

ref(mcp): drop the root .mcp.json compat copy (#319) #70

# Deploy per-agent plugin distributions to their own repositories.
#
# This repository is the single source of truth (skills + routing metadata,
# feeding skills.sentry.dev). Each AI assistant needs the plugin in a different
# shape, so this workflow builds each one and deploys it to a dedicated
# repository whose ROOT is exactly that agent's plugin:
#
# claude -> getsentry/plugin-claude
# cursor -> getsentry/plugin-cursor
# codex -> getsentry/plugin-codex
# grok -> getsentry/plugin-grok
#
# Each plugin repository carries two rolling branches, and which one a run writes
# is the whole difference between a deploy and a release:
#
# develop every push to this repo's main, so its tip is always the latest
# build. Nothing installs from it by default; it is where a change is
# visible and testable before it ships.
# main release tags only, driven by release-plugins.yml. This is the branch
# consumers resolve -- Anthropic's marketplace pins plugin-claude by
# SHA, the installer clones plugin-cursor, and the codex/grok CLIs
# install by repository -- so it moves when we say a version is out,
# not on every merge.
#
# A release also tags the plugin repository (`v<version>`), giving each shipped
# version an addressable ref for pinning and rollback.
#
# Marketplaces consume `getsentry/plugin-<agent>` by git ref. Each job builds its
# agent's tree from this repo, then commits it onto the target branch of the
# target repo, replacing the previous contents. The four jobs target four
# different repos, so they run in parallel without contention.
#
# Cross-repo writes use a GitHub App token scoped per-job to a single plugin
# repo; the default GITHUB_TOKEN cannot push to other repositories. The app must
# be installed on the org with contents:write on the four plugin repos, its ID
# stored as the PLUGIN_DEPLOY_APP_ID variable and its private key as the
# PLUGIN_DEPLOY_KEY secret. Each target repo must already exist with `main` as
# its default branch; `develop` is branched off it on the first deploy.
name: Deploy plugins
on:
push:
branches: [main]
paths:
- "src/skills/**"
- "src/references/**"
- "src/SKILL_TREE.md"
- "src/plugins/**"
- "assets/**"
- "LICENSE"
- "*.json"
- "scripts/**"
- ".github/workflows/deploy-plugins.yml"
workflow_dispatch:
# Called by release-plugins.yml to publish a tagged tree. A tag push cannot
# drive this directly: `paths` filters do not match a push that adds no
# commits, so a tag pointing at an existing commit would silently skip the
# deploy. The release workflow calls in with an explicit ref instead.
workflow_call:
inputs:
ref:
description: Ref of this repository to build from.
type: string
required: true
target_branch:
description: Branch to publish onto in each plugin repository.
type: string
required: true
dist_tag:
description: Tag to create in each plugin repository. Empty creates none.
type: string
required: false
default: ""
permissions:
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
agent: [claude, cursor, codex, grok]
concurrency:
group: deploy-plugin-${{ matrix.agent }}-${{ inputs.target_branch || 'develop' }}
cancel-in-progress: false
steps:
- name: Checkout source
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ inputs.ref || github.sha }}
# scripts/dev-version.sh derives the develop stamp from `git describe`,
# which needs the tags and the commits since the last one.
fetch-depth: 0
- name: Mint deploy token
id: token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ vars.PLUGIN_DEPLOY_APP_ID }}
private-key: ${{ secrets.PLUGIN_DEPLOY_KEY }}
owner: getsentry
repositories: plugin-${{ matrix.agent }}
- name: Install uv
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
- name: Build and deploy plugin-${{ matrix.agent }}
env:
AGENT: ${{ matrix.agent }}
TARGET_BRANCH: ${{ inputs.target_branch || 'develop' }}
DIST_TAG: ${{ inputs.dist_tag || '' }}
GH_TOKEN: ${{ steps.token.outputs.token }}
run: |
set -euo pipefail
TARGET_REPO="plugin-${AGENT}"
WORKTREE="$(mktemp -d)/dist"
# Read the built commit out of the checkout rather than github.sha,
# which in a called workflow reports the caller's commit instead.
SRC_SHA="$(git rev-parse HEAD)"
# A release stamps the version being released; anything else is a
# develop build and gets labelled with its distance from the last tag.
if [[ -z "$DIST_TAG" ]]; then
PLUGIN_VERSION="$(scripts/dev-version.sh)"
export PLUGIN_VERSION
echo "::notice::stamping develop build as ${PLUGIN_VERSION}"
fi
# Clone the target repo (lands on its default branch, `main`).
git clone "https://x-access-token:${GH_TOKEN}@github.com/getsentry/${TARGET_REPO}.git" "$WORKTREE"
git -C "$WORKTREE" config user.name "github-actions[bot]"
git -C "$WORKTREE" config user.email "41898282+github-actions[bot]@users.noreply.github.com"
# Land on the target branch, branching it off the default branch on the
# first deploy that needs it.
if git -C "$WORKTREE" ls-remote --exit-code --heads origin "$TARGET_BRANCH" > /dev/null; then
git -C "$WORKTREE" checkout "$TARGET_BRANCH"
else
echo "::notice::${TARGET_REPO} has no ${TARGET_BRANCH} yet; creating it"
git -C "$WORKTREE" checkout -b "$TARGET_BRANCH"
fi
# Rewrite the whole tree: clear tracked content (preserve the .git
# dir), then repopulate from source via the agent's build script.
git -C "$WORKTREE" rm -rfq --ignore-unmatch .
"src/plugins/${AGENT}/build.sh" "$WORKTREE"
# Validate the built tree against the agent's schema/validator before
# it can be deployed.
"src/plugins/${AGENT}/validate.sh" "$WORKTREE"
# Commit only if something changed.
git -C "$WORKTREE" add -A
if git -C "$WORKTREE" diff --cached --quiet; then
echo "::notice::${TARGET_REPO} ${TARGET_BRANCH} unchanged; nothing to commit"
elif [[ -n "$DIST_TAG" ]]; then
git -C "$WORKTREE" commit -m "build: release ${DIST_TAG} from getsentry/sentry-for-ai@${SRC_SHA}"
else
git -C "$WORKTREE" commit -m "build: deploy from getsentry/sentry-for-ai@${SRC_SHA}"
fi
git -C "$WORKTREE" push origin "$TARGET_BRANCH"
echo "::notice::deployed ${TARGET_REPO} ${TARGET_BRANCH}"
if [[ -z "$DIST_TAG" ]]; then
exit 0
fi
# Tag wherever the branch now points, whether or not this run made a
# commit: a release whose tree already matches main still has to end up
# tagged, and re-running a failed release has to converge on the tag
# rather than skip it.
#
# An existing tag on a different commit is a conflict, not a converged
# retry -- skipping it would leave v<version> naming something other
# than the tree that just shipped. The peeled `^{}` line is what makes
# the comparison hold for an annotated tag, whose own object sha is not
# the commit sha.
TARGET_COMMIT="$(git -C "$WORKTREE" rev-parse HEAD)"
EXISTING_TAG_COMMIT="$(git -C "$WORKTREE" ls-remote --tags origin \
"refs/tags/${DIST_TAG}" "refs/tags/${DIST_TAG}^{}" | tail -1 | cut -f1)"
if [[ -n "$EXISTING_TAG_COMMIT" ]]; then
if [[ "$EXISTING_TAG_COMMIT" != "$TARGET_COMMIT" ]]; then
echo "::error::${TARGET_REPO} ${DIST_TAG} points at ${EXISTING_TAG_COMMIT}, not the ${TARGET_COMMIT} just published"
exit 1
fi
echo "::notice::${TARGET_REPO} already tagged ${DIST_TAG}"
exit 0
fi
git -C "$WORKTREE" tag "$DIST_TAG"
git -C "$WORKTREE" push origin "$DIST_TAG"
echo "::notice::tagged ${TARGET_REPO} ${DIST_TAG}"