-
Notifications
You must be signed in to change notification settings - Fork 2
201 lines (182 loc) · 7.84 KB
/
Copy pathdocker-publish.yml
File metadata and controls
201 lines (182 loc) · 7.84 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
name: Publish container image to GHCR
# Builds the remote-HTTP (`node server/index.js --http`) image OFF-host on
# GitHub-hosted runners and pushes it to GHCR, so the single-CPU IWAC-docker host
# only ever does a lightweight `docker pull` (no on-host build contention).
#
# Triggers on a version tag (v*), or manually. Version tags also publish the
# Claude Desktop / stdio .mcpb bundles and the .agents research skill zip as
# GitHub release assets, then publish the server to the official MCP Registry
# (registry.modelcontextprotocol.io) as io.github.fmadore/iwac-mcp-server.
#
# Ordering is deliberate: typecheck + lint + the full hermetic test suite run
# FIRST, then the release assets are packed, then the image is smoke-tested in a
# container, and only THEN is anything pushed. A red test or a broken pack
# script can no longer ship an image (previously the image — including
# `latest` — was pushed before any verification ran, and no tests ran on the
# tag path at all).
on:
push:
tags:
- "v*"
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }} # fmadore/iwac-mcp-server
# Single source of truth for the Node major used to build and test releases.
# Keep in step with ci.yml, mcpb/Dockerfile and mcpb/package.json `engines`.
# NOTE: this is the BUILD baseline only. scripts/bundle.mjs derives the
# shipped target from manifest.json's `runtimes.node` floor (currently Node
# 20, matching MCP SDK v2), and ci.yml executes the artifact on that floor.
NODE_VERSION: 24
# Pin a specific mcp-publisher release (e.g. "v1.4.0") for reproducible,
# supply-chain-safer publishes; "latest" keeps the old auto-tracking
# behaviour. TODO: set a concrete version after checking
# https://github.com/modelcontextprotocol/registry/releases.
MCP_PUBLISHER_VERSION: latest
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
id-token: write # MCP Registry publish via GitHub OIDC (no secret needed)
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Set up Node
uses: actions/setup-node@v7
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm
cache-dependency-path: mcpb/package-lock.json
- name: Install dependencies
working-directory: mcpb
run: npm ci
# Release gate: the same hermetic suite ci.yml runs on PRs. Nothing below
# (image push, release assets, registry publish) happens if this fails.
- name: Typecheck, lint, build, and test
working-directory: mcpb
run: |
npm run typecheck
npm run lint
npm run build
npm test
- name: Check tag matches package.json/manifest.json versions
if: startsWith(github.ref, 'refs/tags/v')
env:
TAG: ${{ github.ref_name }}
run: |
node -e '
const fs = require("fs");
const tag = process.env.TAG.replace(/^v/, "");
const pkg = JSON.parse(fs.readFileSync("mcpb/package.json", "utf8")).version;
const man = JSON.parse(fs.readFileSync("mcpb/manifest.json", "utf8")).version;
if (tag !== pkg || tag !== man) {
console.error(`version mismatch: tag ${tag}, package.json ${pkg}, manifest.json ${man}`);
process.exit(1);
}
console.log(`versions consistent: ${tag}`);
'
- name: Build MCPB and skill release assets
if: startsWith(github.ref, 'refs/tags/v')
working-directory: mcpb
run: npm run release
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log in to GHCR
uses: docker/login-action@v4.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Derive image tags and labels
id: meta
uses: docker/metadata-action@v6
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# `latest` only moves on version tags — a manual workflow_dispatch
# from a branch must not repoint it.
tags: |
type=semver,pattern={{version}}
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
type=sha,format=short
- name: Build image (local, for smoke test)
uses: docker/build-push-action@v7
with:
context: mcpb
file: mcpb/Dockerfile
platforms: linux/amd64
load: true
tags: iwac-mcp-smoke:ci
cache-from: type=gha
cache-to: type=gha,mode=max
# The Dockerfile's own comment says "a missing binding is a runtime crash,
# not a build error" — so actually boot the container before pushing:
# /health must come up, and /mcp must enforce the bearer token.
- name: Container smoke test
run: |
docker run -d --rm --name iwac-smoke \
-e IWAC_MCP_BEARER_TOKEN=ci-smoke -p 8000:8000 iwac-mcp-smoke:ci
ok=""
for i in $(seq 1 30); do
if curl -fsS http://127.0.0.1:8000/health >/dev/null 2>&1; then ok=1; break; fi
sleep 1
done
if [ -z "$ok" ]; then
echo "container never became healthy"; docker logs iwac-smoke || true; exit 1
fi
code=$(curl -s -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1:8000/mcp)
if [ "$code" != "401" ]; then
echo "expected 401 without bearer token, got $code"; exit 1
fi
docker stop iwac-smoke
- name: Push image (linux/amd64)
uses: docker/build-push-action@v7
with:
context: mcpb
file: mcpb/Dockerfile
platforms: linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Upload GitHub release assets
if: startsWith(github.ref, 'refs/tags/v')
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ github.ref_name }}
run: |
gh release view "$TAG" >/dev/null 2>&1 || gh release create "$TAG" --title "$TAG" --notes "Release $TAG"
gh release upload "$TAG" \
mcpb/iwac-mcp-server-windows.mcpb \
mcpb/iwac-mcp-server-macos.mcpb \
iwac-mcp-skill.zip \
--clobber
# MCP Registry publish. server.json is generated HERE, in the same job
# that packed and uploaded the .mcpb assets, so the embedded fileSha256
# hashes always match the exact uploaded bytes. Registry versions are
# immutable: re-running this workflow for an already-published tag fails
# at this step by design (re-packed zips hash differently) — bump the
# version instead of re-publishing.
- name: Generate server.json
if: startsWith(github.ref, 'refs/tags/v')
env:
TAG: ${{ github.ref_name }}
run: node mcpb/scripts/make-server-json.mjs "$TAG"
- name: Install mcp-publisher
if: startsWith(github.ref, 'refs/tags/v')
run: |
os=$(uname -s | tr '[:upper:]' '[:lower:]')
arch=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')
if [ "$MCP_PUBLISHER_VERSION" = "latest" ]; then
url="https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_${os}_${arch}.tar.gz"
else
url="https://github.com/modelcontextprotocol/registry/releases/download/${MCP_PUBLISHER_VERSION}/mcp-publisher_${os}_${arch}.tar.gz"
fi
curl -fL "$url" | tar xz mcp-publisher
- name: Publish to MCP Registry
if: startsWith(github.ref, 'refs/tags/v')
run: |
./mcp-publisher login github-oidc
./mcp-publisher publish