Skip to content

Commit 232782f

Browse files
committed
ci: gate only live npm publishes
1 parent 0e5e391 commit 232782f

2 files changed

Lines changed: 127 additions & 16 deletions

File tree

.github/workflows/publish-mcp.yml

Lines changed: 121 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,130 @@ concurrency:
1717
group: publish-mcp-${{ github.ref }}
1818
cancel-in-progress: false
1919

20+
env:
21+
PACKAGE_NAME: "@aegis-protocol/mcp-server"
22+
PACKAGE_DIR: mcp
23+
PACK_DIR: .npm-pack
24+
2025
jobs:
26+
verify-mcp-package:
27+
name: Verify MCP package publishability
28+
runs-on: ubuntu-latest
29+
permissions:
30+
contents: read
31+
defaults:
32+
run:
33+
shell: bash
34+
35+
steps:
36+
- name: Check out repository
37+
uses: actions/checkout@v5
38+
with:
39+
persist-credentials: false
40+
submodules: recursive
41+
42+
- name: Set up pnpm
43+
uses: pnpm/action-setup@v4
44+
with:
45+
version: 9
46+
47+
- name: Set up Node.js for npm publish dry run
48+
uses: actions/setup-node@v4
49+
with:
50+
node-version: "24"
51+
registry-url: https://registry.npmjs.org/
52+
53+
- name: Ensure modern npm
54+
run: |
55+
npm install -g npm@latest
56+
node --version
57+
npm --version
58+
node -e '
59+
const minNode = [22, 14, 0];
60+
const node = process.versions.node.split(".").map(Number);
61+
const okNode = node[0] > minNode[0] || (node[0] === minNode[0] && (node[1] > minNode[1] || (node[1] === minNode[1] && node[2] >= minNode[2])));
62+
if (!okNode) {
63+
throw new Error(`npm trusted publishing requires Node >= ${minNode.join(".")}; got ${process.versions.node}`);
64+
}
65+
'
66+
npm --version | node -e '
67+
const fs = require("node:fs");
68+
const version = fs.readFileSync(0, "utf8").trim().split(".").map(Number);
69+
const min = [11, 5, 1];
70+
const ok = version[0] > min[0] || (version[0] === min[0] && (version[1] > min[1] || (version[1] === min[1] && version[2] >= min[2])));
71+
if (!ok) {
72+
throw new Error(`npm trusted publishing requires npm >= ${min.join(".")}; got ${version.join(".")}`);
73+
}
74+
'
75+
76+
- name: Install dependencies
77+
run: pnpm install --frozen-lockfile
78+
79+
- name: Build SDK dependencies
80+
run: pnpm -C sdk build
81+
82+
- name: Typecheck MCP package
83+
run: pnpm -C "$PACKAGE_DIR" typecheck
84+
85+
- name: Run MCP unit tests
86+
run: pnpm -C "$PACKAGE_DIR" test
87+
88+
- name: Build MCP package
89+
run: pnpm -C "$PACKAGE_DIR" build
90+
91+
- name: Resolve package version
92+
id: package
93+
run: |
94+
version=$(node -p "require('./${PACKAGE_DIR}/package.json').version")
95+
echo "version=${version}" >> "$GITHUB_OUTPUT"
96+
echo "Package: ${PACKAGE_NAME}@${version}"
97+
98+
- name: Validate release tag matches package version
99+
if: github.event_name == 'release'
100+
run: |
101+
tag="${GITHUB_REF_NAME#v}"
102+
if [ "$tag" != "${{ steps.package.outputs.version }}" ]; then
103+
echo "Release tag ${GITHUB_REF_NAME} does not match ${PACKAGE_NAME}@${{ steps.package.outputs.version }}" >&2
104+
exit 1
105+
fi
106+
107+
- name: Pack MCP package
108+
id: pack
109+
run: |
110+
rm -rf "$PACK_DIR"
111+
mkdir -p "$PACK_DIR"
112+
(cd "$PACKAGE_DIR" && pnpm pack --pack-destination "$GITHUB_WORKSPACE/$PACK_DIR")
113+
tarball=$(find "$PACK_DIR" -maxdepth 1 -name '*.tgz' -print -quit)
114+
if [ -z "$tarball" ]; then
115+
echo "No package tarball was produced." >&2
116+
exit 1
117+
fi
118+
echo "tarball=${tarball}" >> "$GITHUB_OUTPUT"
119+
tar -tzf "$tarball"
120+
121+
- name: Validate packed package metadata
122+
run: |
123+
tar -xOf "${{ steps.pack.outputs.tarball }}" package/package.json | node -e '
124+
const fs = require("node:fs");
125+
const pkg = JSON.parse(fs.readFileSync(0, "utf8"));
126+
if (pkg.name !== process.env.PACKAGE_NAME) {
127+
throw new Error(`Packed package name mismatch: ${pkg.name}`);
128+
}
129+
const deps = { ...pkg.dependencies, ...pkg.devDependencies, ...pkg.peerDependencies, ...pkg.optionalDependencies };
130+
const bad = Object.entries(deps).filter(([, value]) => String(value).startsWith("workspace:"));
131+
if (bad.length) {
132+
throw new Error(`Packed package still contains workspace protocol dependencies: ${bad.map(([name, value]) => `${name}@${value}`).join(", ")}`);
133+
}
134+
console.log(`${pkg.name}@${pkg.version} metadata OK`);
135+
'
136+
137+
- name: npm publish dry run
138+
run: npm publish "${{ steps.pack.outputs.tarball }}" --access public --dry-run
139+
21140
publish-mcp:
22141
name: Publish @aegis-protocol/mcp-server
142+
needs: verify-mcp-package
143+
if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.publish == true)
23144
runs-on: ubuntu-latest
24145
environment:
25146
name: npm-publish
@@ -30,11 +151,6 @@ jobs:
30151
defaults:
31152
run:
32153
shell: bash
33-
env:
34-
PACKAGE_NAME: "@aegis-protocol/mcp-server"
35-
PACKAGE_DIR: mcp
36-
PACK_DIR: .npm-pack
37-
SHOULD_PUBLISH: ${{ github.event_name == 'release' || inputs.publish == true }}
38154

39155
steps:
40156
- name: Check out repository
@@ -117,7 +233,6 @@ jobs:
117233
fi
118234
119235
- name: Check package version is unpublished before live publish
120-
if: env.SHOULD_PUBLISH == 'true'
121236
run: |
122237
if npm view "${PACKAGE_NAME}@${{ steps.package.outputs.version }}" version >/dev/null 2>&1; then
123238
echo "${PACKAGE_NAME}@${{ steps.package.outputs.version }} is already published; refusing to publish over it." >&2
@@ -154,10 +269,5 @@ jobs:
154269
console.log(`${pkg.name}@${pkg.version} metadata OK`);
155270
'
156271
157-
- name: npm publish dry run
158-
if: env.SHOULD_PUBLISH != 'true'
159-
run: npm publish "${{ steps.pack.outputs.tarball }}" --access public --dry-run
160-
161272
- name: npm publish via trusted publishing
162-
if: env.SHOULD_PUBLISH == 'true'
163273
run: npm publish "${{ steps.pack.outputs.tarball }}" --access public

docs/operations/MCP-NPM-PUBLISHING.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,20 +32,21 @@ The workflow runs on:
3232
- `release.published`
3333
- manual `workflow_dispatch`
3434

35-
Manual runs default to dry-run mode. To publish manually, run the workflow from `main` with `publish=true`.
35+
Manual runs default to dry-run mode. Dry-run verification does not require the `npm-publish` environment approval. To publish manually, run the workflow from `main` with `publish=true`; the live publish job waits for the `npm-publish` environment approval gate.
3636

37-
The workflow:
37+
The verification job:
3838

3939
1. Uses GitHub-hosted `ubuntu-latest`.
40-
2. Grants only `contents: read` and `id-token: write`.
40+
2. Grants only `contents: read`.
4141
3. Installs Node 24 and verifies npm is new enough for trusted publishing.
4242
4. Installs dependencies with `pnpm install --frozen-lockfile`.
4343
5. Builds the SDK packages.
4444
6. Typechecks, tests, and builds the MCP package.
4545
7. Packs the MCP package with `pnpm pack` so `workspace:*` dependencies are rewritten.
4646
8. Validates the packed package metadata has no `workspace:*` dependencies.
47-
9. Runs `npm publish --dry-run` unless this is a release or manual `publish=true` run.
48-
10. Publishes with `npm publish` when live publishing is enabled.
47+
9. Runs `npm publish --dry-run`.
48+
49+
The live publish job repeats the same build/test/pack validation after the environment approval, then publishes with `npm publish` using GitHub Actions OIDC.
4950

5051
## Release process
5152

0 commit comments

Comments
 (0)