-
-
Notifications
You must be signed in to change notification settings - Fork 0
421 lines (380 loc) · 17.8 KB
/
Copy pathrelease.yml
File metadata and controls
421 lines (380 loc) · 17.8 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
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
name: Release
on:
push:
tags:
- "v*.*.*"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
preflight:
# Fail within seconds if WINGET_GITHUB_TOKEN is expired or revoked,
# instead of 14 minutes into GoReleaser — after it has already created
# the draft release and opened the Homebrew/Scoop PRs — which strands
# the release in a half-published state (see the v0.18.2 run). The
# winget publisher is the only one on a long-lived PAT: the release-bot
# app token cannot open PRs on microsoft/winget-pkgs, where the app is
# not installed.
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Check winget token validity and expiry
env:
WINGET_GITHUB_TOKEN: ${{ secrets.WINGET_GITHUB_TOKEN }}
run: |
set -euo pipefail
headers_file="$(mktemp)"
status="$(curl -sS -o /dev/null -D "$headers_file" -w '%{http_code}' \
-H "Authorization: Bearer ${WINGET_GITHUB_TOKEN}" \
https://api.github.com/repos/bomly-dev/winget-pkgs)"
if [ "$status" != "200" ]; then
echo "::error::WINGET_GITHUB_TOKEN cannot read bomly-dev/winget-pkgs (HTTP ${status}). The PAT has likely expired or been revoked — mint a new one (public_repo scope, it must be able to open PRs on microsoft/winget-pkgs) and update the secret: gh secret set WINGET_GITHUB_TOKEN -R bomly-dev/bomly-cli"
exit 1
fi
expiry="$(grep -i '^github-authentication-token-expiration:' "$headers_file" | cut -d' ' -f2- | tr -d '\r' || true)"
if [ -n "$expiry" ]; then
expiry_epoch="$(date -u -d "$expiry" +%s)"
days_left=$(( (expiry_epoch - $(date -u +%s)) / 86400 ))
echo "WINGET_GITHUB_TOKEN expires ${expiry} UTC (${days_left} days from now)."
if [ "$days_left" -lt 14 ]; then
echo "::warning::WINGET_GITHUB_TOKEN expires in ${days_left} days (${expiry} UTC). Rotate it soon: gh secret set WINGET_GITHUB_TOKEN -R bomly-dev/bomly-cli"
fi
else
echo "WINGET_GITHUB_TOKEN has no expiration date."
fi
validate:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
cache: true
cache-dependency-path: |
go.sum
- name: Run test suite
run: make test
- name: Run go vet
run: go vet ./...
release:
needs: [preflight, validate]
runs-on: ubuntu-latest
timeout-minutes: 45
permissions:
contents: write
packages: write
id-token: write # required for cosign keyless signing
outputs:
hashes: ${{ steps.hash.outputs.hashes }}
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
cache: true
cache-dependency-path: |
go.sum
- name: Mint package-manager token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
id: package-token
with:
client-id: ${{ vars.RELEASE_BOT_CLIENT_ID }}
private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }}
owner: bomly-dev
repositories: |
homebrew-tap
scoop-bucket
permission-contents: write
permission-pull-requests: write
- name: Mint release token
# Used as GoReleaser's GITHUB_TOKEN to create the draft release on this
# repo and upload its assets (archives, packages, checksums, signature).
# The release stays a draft here (see .goreleaser.yaml) — actual
# publishing happens in the `publish` job below, after the SLSA
# provenance file is also attached.
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
id: release-token
with:
client-id: ${{ vars.RELEASE_BOT_CLIENT_ID }}
private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }}
owner: bomly-dev
repositories: bomly-cli
permission-contents: write
- name: Install cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3
with:
distribution: goreleaser
version: v2.16.0
args: release --clean
env:
GITHUB_TOKEN: ${{ steps.release-token.outputs.token }}
TAP_GITHUB_TOKEN: ${{ steps.package-token.outputs.token }}
SCOOP_GITHUB_TOKEN: ${{ steps.package-token.outputs.token }}
WINGET_GITHUB_TOKEN: ${{ secrets.WINGET_GITHUB_TOKEN }}
- name: Generate artifact hashes for SLSA provenance
id: hash
run: echo "hashes=$(base64 -w0 < dist/SHA256SUMS)" >> "$GITHUB_OUTPUT"
provenance:
needs: release
permissions:
actions: read # detect the workflow that triggered this run
id-token: write # sign the provenance
contents: write # upload the provenance to the release
# The SLSA generator MUST be pinned by a full vX.Y.Z tag, not a commit SHA or
# shorter tag — slsa-verifier resolves the builder identity from this exact ref,
# and SHA-pinning breaks that resolution. See .github/pinact.yaml, which excludes
# this line from automated re-pinning.
#
# upload-assets is deliberately false: its underlying action
# (softprops/action-gh-release) finds a release by tag, and GitHub's
# "get release by tag" API does not return drafts. Against our still-draft
# release, it can't find one — so it creates a *second*, non-draft release
# for the same tag instead, which immediately and permanently marks that
# tag as immutable (even if the bad release is later deleted). The
# `publish` job uploads this provenance file itself, by release ID, to
# avoid that entirely.
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0
with:
base64-subjects: ${{ needs.release.outputs.hashes }}
upload-assets: false
publish:
needs: [release, provenance]
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Mint release token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
id: release-token
with:
client-id: ${{ vars.RELEASE_BOT_CLIENT_ID }}
private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }}
owner: bomly-dev
repositories: bomly-cli
permission-contents: write
- name: Download SLSA provenance
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ${{ needs.provenance.outputs.provenance-name }}
- name: Attach provenance and publish release
# Uploads the provenance file directly to the draft release by ID
# (not by tag — see the `provenance` job comment for why), then
# finalizes the draft into a published release now that every asset
# is attached. Must run last: immutable releases reject new assets
# once published.
#
# Uses the release-bot app token, not the default GITHUB_TOKEN: GitHub
# does not start new workflow runs from events triggered by the
# default token, so a release published with it would never fire the
# `release: published` trigger on "Release lifecycle sync"
# (notify-landing-yank.yml) — and the landing-page docs sync would
# silently never run. An app token is attributed to the app, so the
# publish event cascades.
env:
GH_TOKEN: ${{ steps.release-token.outputs.token }}
RELEASE_TAG: ${{ github.ref_name }}
PROVENANCE_FILE: ${{ needs.provenance.outputs.provenance-name }}
run: |
set -euo pipefail
release_id="$(gh api "repos/bomly-dev/bomly-cli/releases?per_page=100" | \
jq -r --arg tag "$RELEASE_TAG" '.[] | select(.tag_name == $tag and .draft == true) | .id' | head -n1)"
if [ -z "$release_id" ]; then
echo "::error::No draft release found for tag ${RELEASE_TAG}"
exit 1
fi
curl -fsSL \
-X POST \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "Content-Type: application/octet-stream" \
--data-binary "@${PROVENANCE_FILE}" \
"https://uploads.github.com/repos/bomly-dev/bomly-cli/releases/${release_id}/assets?name=${PROVENANCE_FILE}" \
> /dev/null
gh release edit "$RELEASE_TAG" --draft=false --repo bomly-dev/bomly-cli
publish-npm:
# Publishes the `bomly-mcp` npm wrapper, which is the package the official
# MCP Registry entry points at.
#
# Authentication is npm Trusted Publishing (OIDC) — there is deliberately
# no npm token here, and none should ever be added. npm's own token screen
# warns against long-lived automation tokens for exactly this case.
#
# Must run after `publish`: the wrapper's postinstall step downloads the
# release archive and its checksum file from this tag, and draft-release
# assets are not publicly downloadable — so publishing npm any earlier
# ships a package that cannot install.
#
# Split from `publish-mcp-registry` so a failure in either half can be
# re-run on its own. npm versions are immutable, so this job also treats
# "this exact version is already published" as success rather than as an
# error that would block the registry step behind it.
#
# Inert until Ahmed turns it on. Setup is one-time and documented in
# the short version:
# 1. publish bomly-mcp once by hand (trusted publishing cannot create a
# package that does not exist yet)
# 2. set the trusted publisher at
# npmjs.com/package/bomly-mcp/access -> this repo, workflow release.yml
# 3. gh variable set PUBLISH_MCP_REGISTRY -R bomly-dev/bomly-cli --body true
needs: publish
if: startsWith(github.ref, 'refs/tags/') && vars.PUBLISH_MCP_REGISTRY == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
# Named in npm's trusted-publisher config, which checks it as an OIDC
# claim: publishing fails unless the two match. Restrict this environment
# to the tag pattern `v*` so a push to any branch cannot reach npm.
#
# Deliberately NOT the existing `release` environment — that one is scoped
# to the `main` branch, and this job runs on a tag ref, so it would be
# rejected outright.
environment: npm-publish
permissions:
contents: read
id-token: write # mints the OIDC token npm exchanges for publish rights
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Ensure npm supports trusted publishing
# Trusted publishing needs npm >= 11.5.1, which the runner image has
# not always bundled. Upgrade only when the shipped npm is too old, so
# this becomes a no-op once the image catches up.
#
# `sudo` is required: npm's global prefix on the runner is /usr/local,
# which the `runner` user cannot write to. Without it this fails with
# EACCES on /usr/local/share/man — which is exactly how the v0.21.2
# release lost its npm and registry publish.
run: |
set -euo pipefail
required=11.5.1
current="$(npm --version)"
if printf '%s\n%s\n' "$required" "$current" | sort -V -C; then
echo "npm ${current} already supports trusted publishing."
else
echo "npm ${current} is older than ${required}; upgrading."
sudo npm install -g "npm@^${required}"
fi
npm --version
- name: Stamp the release version into npm/package.json
# The tag is authoritative. Auto Version keeps the committed files in
# sync too, but deriving here means a missed bump cannot publish a
# wrapper that points at a release that does not exist.
env:
RELEASE_TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
version="${RELEASE_TAG#v}"
jq --arg v "$version" '.version = $v' npm/package.json > npm/package.json.tmp
mv npm/package.json.tmp npm/package.json
- name: Publish bomly-mcp to npm
# No `env:` and no npmrc: npm reads the OIDC token from the Actions
# runtime itself. --provenance is passed explicitly even though trusted
# publishing is documented to imply it, because the implicit path has
# been reported to no-op.
env:
RELEASE_TAG: ${{ github.ref_name }}
working-directory: npm
run: |
set -euo pipefail
version="${RELEASE_TAG#v}"
# Idempotent: a re-run after a partial failure must not die here on
# an immutable version that is already live.
if curl -fsSL "https://registry.npmjs.org/bomly-mcp/${version}" > /dev/null 2>&1; then
echo "bomly-mcp@${version} is already published; skipping npm publish."
exit 0
fi
npm publish --provenance --access public
publish-mcp-registry:
# Publishes the server entry in the official MCP Registry
# (https://registry.modelcontextprotocol.io).
#
# Registry auth is GitHub OIDC, so it needs no secret of its own; the
# `io.github.bomly-dev/*` namespace is granted from this repo's identity.
#
# Kept separate from publish-npm so either half can be re-run alone: npm
# versions are immutable, so a combined job that failed here could not be
# retried without tripping over the already-published version. (It was also
# the boundary that kept an npm token away from the downloaded
# mcp-publisher binary; with trusted publishing there is no token left to
# isolate, but the retry argument stands on its own.)
needs: publish-npm
if: startsWith(github.ref, 'refs/tags/') && vars.PUBLISH_MCP_REGISTRY == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
id-token: write # required for mcp-publisher's OIDC login
env:
# Pinned deliberately. `releases/latest` would download and execute a
# moving target inside a job that holds an OIDC identity able to publish
# under our registry namespace. Bump this together with the digest and
# the signer identity below, after reading the release diff.
MCP_PUBLISHER_VERSION: v1.8.0
MCP_PUBLISHER_SHA256: 1370446bbe74d562608e8005a6ccce02d146a661fbd78674e11cc70b9618d6cf
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Stamp the release version into server.json
env:
RELEASE_TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
version="${RELEASE_TAG#v}"
jq --arg v "$version" '.version = $v | .packages[0].version = $v' server.json > server.json.tmp
mv server.json.tmp server.json
- name: Wait for the npm package to be resolvable
# The registry verifies package ownership by fetching the package, so
# publishing the server entry before npm's CDN has the new version
# fails validation.
env:
RELEASE_TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
version="${RELEASE_TAG#v}"
for attempt in $(seq 1 30); do
if curl -fsSL "https://registry.npmjs.org/bomly-mcp/${version}" > /dev/null 2>&1; then
echo "bomly-mcp@${version} is resolvable."
exit 0
fi
echo "Attempt ${attempt}: bomly-mcp@${version} not visible yet, waiting."
sleep 10
done
echo "::error::bomly-mcp@${version} did not become resolvable on npm within 5 minutes"
exit 1
- name: Install cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: Download and verify mcp-publisher
# Two independent checks before anything is extracted or executed:
# the pinned SHA-256, and the Sigstore bundle proving the archive was
# built by the registry's own tagged release workflow.
run: |
set -euo pipefail
base="https://github.com/modelcontextprotocol/registry/releases/download/${MCP_PUBLISHER_VERSION}"
archive="mcp-publisher_linux_amd64.tar.gz"
curl -fsSL -o "${archive}" "${base}/${archive}"
curl -fsSL -o "${archive}.sigstore.json" "${base}/${archive}.sigstore.json"
echo "${MCP_PUBLISHER_SHA256} ${archive}" | sha256sum --check --strict
cosign verify-blob "${archive}" \
--bundle "${archive}.sigstore.json" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
--certificate-identity "https://github.com/modelcontextprotocol/registry/.github/workflows/release.yml@refs/tags/${MCP_PUBLISHER_VERSION}"
tar xzf "${archive}" mcp-publisher
- name: Publish the server entry to the MCP Registry
run: |
set -euo pipefail
./mcp-publisher login github-oidc
./mcp-publisher publish