diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 556c57b2ba..aa2e410ec5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -232,6 +232,41 @@ jobs: with: release_tag: v${{ needs.plan.outputs.version }} + # agent-device.dev serves /.well-known/mcp.json with the npm `latest` version read when the + # site builds. A tag push builds nothing on Vercel, so ask for a production build of main once + # npm serves the new version. The VERCEL_SITE_DEPLOY_HOOK secret holds the Vercel deploy hook + # of the agent-device-website project for main; it is the only secret this workflow reads. + redeploy-site: + name: Redeploy agent-device.dev + needs: [plan, publish] + if: needs.plan.outputs.mode == 'stable' + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: {} + steps: + # Any latest at or past this version will do: a newer stable release may already have + # moved latest on, and the site only needs the current one. + - name: Wait for npm to serve this release or a newer one as latest + env: + VERSION: ${{ needs.plan.outputs.version }} + run: | + for attempt in $(seq 1 30); do + latest="$(npm view agent-device@latest version 2>/dev/null || true)" + if [ -n "$latest" ] && + [ "$(printf '%s\n' "$VERSION" "$latest" | sort -V | head -n 1)" = "$VERSION" ]; then + echo "npm serves $latest as latest." + exit 0 + fi + sleep 10 + done + echo "npm does not serve $VERSION or newer as latest after 5 minutes." + exit 1 + + - name: Trigger the site's production build + env: + DEPLOY_HOOK: ${{ secrets.VERCEL_SITE_DEPLOY_HOOK }} + run: curl -fsS -X POST "$DEPLOY_HOOK" > /dev/null + mark-released: name: Move main past ${{ needs.plan.outputs.version }} needs: [plan, publish] diff --git a/test/ci/release-workflow.test.ts b/test/ci/release-workflow.test.ts index ca4be4eb67..7f4fb51541 100644 --- a/test/ci/release-workflow.test.ts +++ b/test/ci/release-workflow.test.ts @@ -26,8 +26,20 @@ test('only the npm publisher and the MCP Registry call can mint an id-token', () expect(workflow.jobs.publish?.environment).toBe('npm-publish'); }); +/** The secret names a job references, wherever in the job they appear. */ +const secretsOf = (job: Job) => + [...JSON.stringify(job).matchAll(/secrets\.([A-Za-z0-9_]+)/g)].map(([, name]) => name); + test('the publisher authenticates through OIDC alone', () => { - expect(source).not.toMatch(/NPM_TOKEN|NODE_AUTH_TOKEN|secrets\./); + expect(source).not.toMatch(/NPM_TOKEN|NODE_AUTH_TOKEN/); + // No secret reaches the jobs that build or publish the packages. The one secret in the file + // is the site's deploy hook, which only the redeploy job after a stable release reads. + expect(secretsOf(workflow.jobs.publish ?? {})).toEqual([]); + expect(secretsOf(workflow.jobs.build ?? {})).toEqual([]); + const withSecrets = Object.entries(workflow.jobs) + .map(([name, job]) => [name, secretsOf(job)] as const) + .filter(([, names]) => names.length > 0); + expect(withSecrets).toEqual([['redeploy-site', ['VERCEL_SITE_DEPLOY_HOOK']]]); const setupNode = workflow.jobs.publish?.steps?.find((step) => step.uses?.startsWith('actions/setup-node@'), ); diff --git a/website/src/pages/.well-known/mcp.json.ts b/website/src/pages/.well-known/mcp.json.ts index ea82de1cff..37a2d90d25 100644 --- a/website/src/pages/.well-known/mcp.json.ts +++ b/website/src/pages/.well-known/mcp.json.ts @@ -1,8 +1,9 @@ // /.well-known/mcp.json: the stdio MCP server's registry identity, from the repo's server.json, -// with the version set to the latest one published on npm when the site is built. The site -// redeploys on every push to main, so the version follows releases. Without npm the build -// fails: server.json between releases carries an unpublished `-dev` version, and agents would -// be told to install a package that does not exist. +// with the version set to the latest one published on npm when the site is built. The release +// workflow (release.yml, redeploy-site) rebuilds the site right after each stable release, so +// the version follows releases. Without npm the build fails: server.json between releases +// carries an unpublished `-dev` version, and agents would be told to install a package that +// does not exist. import type { APIRoute } from 'astro'; import server from '../../../../server.json';