-
-
Notifications
You must be signed in to change notification settings - Fork 32
219 lines (199 loc) · 9.28 KB
/
Copy pathdeploy-plugins.yml
File metadata and controls
219 lines (199 loc) · 9.28 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# 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:
#
# agent-plugin -> getsentry/agent-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.
#
# Consumers install a distribution repository by git ref. Each job builds its
# distribution tree from this repo, then commits it onto the target branch of the
# target repo, replacing the previous contents. The five jobs target five
# 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: ""
secrets:
# The one secret this workflow uses, declared so a caller passes it
# by name rather than handing over every secret in the repository
# with `secrets: inherit`. Its own push and workflow_dispatch runs
# read the repository secret directly, which is why it is optional.
PLUGIN_DEPLOY_KEY:
description: Private key of the plugin-deploy GitHub App.
required: false
permissions:
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
include:
- distribution: agent-plugin
repository: agent-plugin
- distribution: claude
repository: plugin-claude
- distribution: cursor
repository: plugin-cursor
- distribution: codex
repository: plugin-codex
- distribution: grok
repository: plugin-grok
concurrency:
group: deploy-${{ matrix.repository }}-${{ 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: ${{ matrix.repository }}
- name: Install uv
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
- name: Build and deploy ${{ matrix.repository }}
env:
DISTRIBUTION: ${{ matrix.distribution }}
TARGET_REPO: ${{ matrix.repository }}
TARGET_BRANCH: ${{ inputs.target_branch || 'develop' }}
DIST_TAG: ${{ inputs.dist_tag || '' }}
GH_TOKEN: ${{ steps.token.outputs.token }}
run: |
set -euo pipefail
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/${DISTRIBUTION}/build.sh" "$WORKTREE"
# Validate the built tree against the agent's schema/validator before
# it can be deployed.
"src/plugins/${DISTRIBUTION}/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}"