From cb3910741e6cd3d37832111e4a108b4088f9e636 Mon Sep 17 00:00:00 2001 From: CD Cabrera Date: Mon, 26 Oct 2026 15:28:40 -0400 Subject: [PATCH 1/7] ci: pf-4355 container publish workflow * workflows/container, decouple audit and publish jobs, add multi-arch release push to ghcr * package.json, add container test scripts * docs/usage, add hosted container instructions, update http and smoke test examples * config, update mcp-config-container-example to hosted ghcr image * cspell, add pfmcp to dictionary --- .github/workflows/container.yml | 92 ++++++++++++++++++++++++++++--- cspell.config.json | 1 + docs/usage.md | 44 ++++++++++++++- mcp-config-container-example.json | 4 +- package.json | 3 +- 5 files changed, 130 insertions(+), 14 deletions(-) diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml index 96aaffbe..ea2a6d4d 100644 --- a/.github/workflows/container.yml +++ b/.github/workflows/container.yml @@ -1,6 +1,8 @@ name: Container on: + release: + types: [ published ] pull_request: paths: - '.containerignore' @@ -9,25 +11,99 @@ on: - 'tests/container/**' - 'package.json' - 'package-lock.json' - workflow_dispatch: + push: + branches: [ "main" ] + paths: + - '.containerignore' + - 'Containerfile' + - 'scripts/container.*' + - 'tests/container/**' + - 'package.json' + - 'package-lock.json' + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} jobs: container-audit: name: Container Audit runs-on: ubuntu-latest timeout-minutes: 15 - permissions: - contents: read steps: - - uses: actions/checkout@v7 + - name: Checkout repository + uses: actions/checkout@v7 + - name: Setup Node.js uses: actions/setup-node@v7.0.0 with: node-version: '24' cache: npm + - name: Install dependencies run: npm ci - - name: Run container audit - run: npm run test:audit-container - # Advisories are non-blocking for PRs - continue-on-error: ${{ github.event_name == 'pull_request' }} + + - name: Build local image for verification + uses: redhat-actions/buildah-build@v3 + with: + image: localhost/patternfly-mcp + tags: latest + containerfiles: ./Containerfile + + - name: Audit Container + run: npm run container:test + + publish: + name: Container Image Publish + runs-on: ubuntu-latest + timeout-minutes: 25 + needs: [ container-audit ] + # Explicitly locks down execution to formal GitHub release publications only + if: | + github.repository == 'patternfly/patternfly-mcp' && + github.event_name == 'release' && + github.event.action == 'published' + permissions: + contents: read + packages: write + steps: + - name: Checkout repository + uses: actions/checkout@v7 + + - name: Log in to registry + uses: docker/login-action@v4 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Generates 'vX.Y.Z' and 'latest' from your Git Release Tag natively + - name: Setup OCI metadata + id: meta + uses: docker/metadata-action@v6 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=semver,pattern={{version}} + type=raw,value=latest + + - name: Setup QEMU for multi-arch + uses: docker/setup-qemu-action@v4 + with: + platforms: 'linux/amd64,linux/arm64' + + - name: Build multi-arch image + id: build-image + uses: redhat-actions/buildah-build@v3 + with: + image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: ${{ steps.meta.outputs.tags }} + archs: amd64, arm64 + containerfiles: ./Containerfile + + - name: Push image to registry + uses: redhat-actions/push-to-registry@v3 + with: + image: ${{ steps.build-image.outputs.image }} + tags: ${{ steps.meta.outputs.tags }} + registry: ${{ env.REGISTRY }} diff --git a/cspell.config.json b/cspell.config.json index 13842167..a1452541 100644 --- a/cspell.config.json +++ b/cspell.config.json @@ -16,6 +16,7 @@ "nosniff", "onsessioninitialized", "onsessionclosed", + "pfmcp", "patternfly", "prefault", "pypy", diff --git a/docs/usage.md b/docs/usage.md index 17848ca1..d6b3e608 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -222,6 +222,36 @@ The server can also be launched from a container image instead of `npx`. This is > > We make a minimal effort to support Docker, and additional configuration may be required. +#### Use the hosted image + +Update your MCP configuration to use the hosted container image, that's it! + +> We make allowances for `amd64` and `arm64` architectures which should cover most use cases across Linux and macOS. In the event you need access to a different architecture, you can either build your own image from source (see below) or open an issue requesting the new architecture. + +```json +{ + "mcpServers": { + "patternfly-mcp": { + "command": "podman", + "env": { + "PODMAN_USERNS": "keep-id" + }, + "args": [ + "run", + "--rm", + "-i", + "--security-opt=no-new-privileges", + "--cap-drop=ALL", + "ghcr.io/patternfly/patternfly-mcp:latest", + "--log-stderr" + ], + "description": "PatternFly rules and documentation" + } + } +} +``` + + #### Build the image locally From the repository root: @@ -277,8 +307,16 @@ You can confirm by running `$ podman images` from the terminal. View the [Contai #### Smoke test +Confirm the container image is available + +```bash +podman run --rm --entrypoint node ghcr.io/patternfly/patternfly-mcp:latest -e "console.log(process.versions.node)" +``` + +Or confirm a locally built container image is available + ```bash -podman run --rm --entrypoint node localhost/patternfly-mcp:latest -e "console.log(process.versions.node)" # -> 24.x +podman run --rm --entrypoint node localhost/patternfly-mcp:latest -e "console.log(process.versions.node)" ``` #### Running in HTTP transport mode @@ -293,7 +331,7 @@ podman run --rm \ --read-only \ --tmpfs /tmp:rw,size=64m \ -p 3030:8080 \ - localhost/patternfly-mcp:latest \ + ghcr.io/patternfly/patternfly-mcp:latest \ --http \ --port 8080 \ --allowed-origins "http://127.0.0.1:3030" \ @@ -324,7 +362,7 @@ HTTP MCP clients connect via URL rather than spawning a process. Start the conta } ``` -> Stdio-only MCP clients should keep using the [stdio container configuration above](#running-your-local-image-mcp-client-configuration); the HTTP form requires a client that supports HTTP/SSE MCP transports. +> Stdio-only MCP clients should keep using the [stdio container configuration above](#use-the-hosted-image); the HTTP form requires a client that supports HTTP/SSE MCP transports. ##### Verify it's listening diff --git a/mcp-config-container-example.json b/mcp-config-container-example.json index 70ff2eff..d5f7d495 100644 --- a/mcp-config-container-example.json +++ b/mcp-config-container-example.json @@ -11,10 +11,10 @@ "-i", "--security-opt=no-new-privileges", "--cap-drop=ALL", - "localhost/patternfly-mcp:latest", + "ghcr.io/patternfly/patternfly-mcp:latest", "--log-stderr" ], - "description": "PatternFly rules and documentation (local container)" + "description": "PatternFly rules and documentation" } } } diff --git a/package.json b/package.json index 456e93c9..74abd689 100644 --- a/package.json +++ b/package.json @@ -40,12 +40,13 @@ "build:collections-docs": "UPDATE_COLLECTIONS=true tsx ./tooling/collections/update.patternFlyDocs.ts", "container:build": "bash ./scripts/container.build.sh", "container:start": "bash ./scripts/container.run.sh", + "container:test": "jest --selectProjects audit:container", "release": "changelog --non-cc --link-url https://github.com/patternfly/patternfly-mcp.git", "start": "node dist/cli.js --log-stderr", "start:dev": "tsx watch src/cli.ts --verbose --log-stderr", "test": "npm run test:spell && npm run test:spell-docs && npm run test:lint && npm run test:types && jest --selectProjects unit", "test:audit": "NODE_OPTIONS='--experimental-vm-modules' jest --selectProjects audit", - "test:audit-container": "npm run container:build && jest --selectProjects audit:container", + "test:audit-container": "npm run container:build && npm run container:test", "test:ci": "npm test -- --coverage", "test:collections": "jest --selectProjects tooling:collections", "test:collections-dev": "npm run test:collections -- --watchAll", From 65d36e9d0e41c75ae3642a1d8be1bd0f8bf7bfd5 Mon Sep 17 00:00:00 2001 From: CD Cabrera Date: Thu, 29 Oct 2026 00:54:40 -0400 Subject: [PATCH 2/7] fix: review update --- tests/container/utils/containerClient.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/container/utils/containerClient.ts b/tests/container/utils/containerClient.ts index a727c30c..080f46a0 100644 --- a/tests/container/utils/containerClient.ts +++ b/tests/container/utils/containerClient.ts @@ -36,7 +36,11 @@ const buildImage = async (engine: 'podman' | 'docker', image: string) => { try { execSync(`${engine} image inspect ${image}`, { stdio: 'ignore' }); } catch { - console.warn(`Image ${image} not found. Building...`); + if (process.env.CI) { + throw new Error(`[CI] Pre-built container image "${image}" not found.`); + } + + console.warn(`Image ${image} not found. Building locally...`); execSync('npm run container:build', { stdio: 'inherit' }); } }; From 0a7269211aa9ad48f391f39f0c9ee325a5ed50a8 Mon Sep 17 00:00:00 2001 From: CD Cabrera Date: Thu, 29 Oct 2026 01:04:57 -0400 Subject: [PATCH 3/7] fix: review update --- tests/container/container.audit.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/container/container.audit.test.ts b/tests/container/container.audit.test.ts index 7485f519..2264974f 100644 --- a/tests/container/container.audit.test.ts +++ b/tests/container/container.audit.test.ts @@ -9,13 +9,13 @@ describeSkip(engine !== undefined)('Container Audit', () => { CLIENT = await startContainer({ engine }); - }); + }, 60_000); afterAll(async () => { if (CLIENT) { await CLIENT.stop(); } - }); + }, 30_000); it('should start and have basic tools and resources', async () => { const tools = await CLIENT.send({ method: 'tools/list' }); From 604c9c99a8079988d90b9add07d223298e88141b Mon Sep 17 00:00:00 2001 From: CD Cabrera Date: Thu, 29 Oct 2026 02:18:18 -0400 Subject: [PATCH 4/7] fix: review update --- .github/workflows/container.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml index ea2a6d4d..da001e29 100644 --- a/.github/workflows/container.yml +++ b/.github/workflows/container.yml @@ -58,11 +58,13 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 25 needs: [ container-audit ] - # Explicitly locks down execution to formal GitHub release publications only + # Explicitly locks down execution to stable GitHub release publications only if: | github.repository == 'patternfly/patternfly-mcp' && github.event_name == 'release' && - github.event.action == 'published' + github.event.action == 'published' && + !github.event.release.prerelease && + !contains(github.event.release.tag_name, '-') permissions: contents: read packages: write @@ -84,7 +86,7 @@ jobs: with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | - type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}}.{{patch}} type=raw,value=latest - name: Setup QEMU for multi-arch From d89798005989c7b56d0cdb537c4e1ae5b3d37b52 Mon Sep 17 00:00:00 2001 From: CD Cabrera Date: Thu, 29 Oct 2026 17:05:04 -0400 Subject: [PATCH 5/7] fix: review update --- .github/workflows/container.yml | 2 +- docs/development.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml index da001e29..f6abc421 100644 --- a/.github/workflows/container.yml +++ b/.github/workflows/container.yml @@ -79,7 +79,7 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - # Generates 'vX.Y.Z' and 'latest' from your Git Release Tag natively + # Generates 'X.Y.Z' and 'latest' from your Git Release Tag natively - name: Setup OCI metadata id: meta uses: docker/metadata-action@v6 diff --git a/docs/development.md b/docs/development.md index d13e491a..538ef7f4 100644 --- a/docs/development.md +++ b/docs/development.md @@ -499,18 +499,18 @@ Start on a **fork** of the repository, then: 6. A git release is then created mirroring previous git releases. - Use tags that are prefixed with `v`, (e.g. `v1.0.0`, `v1.2.1`, etc.) - Do not "auto-generate" the release notes, prefer linking to the [CHANGELOG.md](../CHANGELOG.md), this prevents note drift. -7. After a release is created and tagged, `CI` will run and publish the related NPM package. +7. After a release is created and tagged, `CI` will run and publish the related NPM package and the container image to the GitHub Container Registry (GHCR) for Patternfly. 8. Perform the [Post-release review](#post-release-review). ### Post-release review -After each **npm release**, run a maintainer pass in addition to **CI** (integration, scheduled jobs in `audit.yml` / `container.yml`, publish workflow). CI is the automated gate; this is a smoke test to confirm everything is in working order. +After each release, run a maintainer pass in addition to **CI** (integration, scheduled jobs in `audit.yml` / `container.yml`, publish workflow). CI is the automated gate; this is a smoke test to confirm everything is in working order. **When:** Each GitHub Release. Ad hoc when major features are introduced, collections, allowlists, or experimental flags change materially. #### Default checklist -- [ ] Version on npm matches tag and [CHANGELOG.md](../CHANGELOG.md). +- [ ] Version on npm and GitHub Container Registry (GHCR) for PatternFly matches both tag and [CHANGELOG.md](../CHANGELOG.md); Confirm the `:latest` GHCR image is also available. - [ ] Collections/sources: any change to `docs.json`, API seed/crawl, schema pin, or ai-handbook wiring? - [ ] Built-in tools are still read-only; experimental surface matches [experimental.md](./experimental.md). - [ ] Confirm allowlist / HTTP docs are still accurate (`src/options.defaults.ts`, [usage.md](./usage.md)). From fe95e645705e3fc6af6c0e1647c118ef6ce995c8 Mon Sep 17 00:00:00 2001 From: CD Cabrera Date: Thu, 29 Oct 2026 17:18:29 -0400 Subject: [PATCH 6/7] fix: review update --- docs/development.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/development.md b/docs/development.md index 538ef7f4..84f9b43d 100644 --- a/docs/development.md +++ b/docs/development.md @@ -499,18 +499,19 @@ Start on a **fork** of the repository, then: 6. A git release is then created mirroring previous git releases. - Use tags that are prefixed with `v`, (e.g. `v1.0.0`, `v1.2.1`, etc.) - Do not "auto-generate" the release notes, prefer linking to the [CHANGELOG.md](../CHANGELOG.md), this prevents note drift. -7. After a release is created and tagged, `CI` will run and publish the related NPM package and the container image to the GitHub Container Registry (GHCR) for Patternfly. +7. Once the GitHub Release is published, CI will automatically publish the npm package to the npm registry and the multi-arch container image to GitHub Container Registry (`ghcr.io/patternfly/patternfly-mcp`). 8. Perform the [Post-release review](#post-release-review). ### Post-release review -After each release, run a maintainer pass in addition to **CI** (integration, scheduled jobs in `audit.yml` / `container.yml`, publish workflow). CI is the automated gate; this is a smoke test to confirm everything is in working order. +After each release, run a maintainer pass in addition to **CI** (integration checks in `integration.yml`, release publishing in `publishing.yml` and `container.yml`). CI is the automated gate; this is a smoke test to confirm everything is in working order. **When:** Each GitHub Release. Ad hoc when major features are introduced, collections, allowlists, or experimental flags change materially. #### Default checklist -- [ ] Version on npm and GitHub Container Registry (GHCR) for PatternFly matches both tag and [CHANGELOG.md](../CHANGELOG.md); Confirm the `:latest` GHCR image is also available. +- [ ] Version on npm and GHCR matches [CHANGELOG.md](../CHANGELOG.md) and corresponds to the Git tag (omitting the `v` prefix). +- [ ] Confirm the `:latest` GHCR image tag is updated and resolves to the new release. - [ ] Collections/sources: any change to `docs.json`, API seed/crawl, schema pin, or ai-handbook wiring? - [ ] Built-in tools are still read-only; experimental surface matches [experimental.md](./experimental.md). - [ ] Confirm allowlist / HTTP docs are still accurate (`src/options.defaults.ts`, [usage.md](./usage.md)). From 2478aa40c6eda22d4ce7e4b673a90499bd1c490b Mon Sep 17 00:00:00 2001 From: CD Cabrera Date: Thu, 29 Oct 2026 17:25:23 -0400 Subject: [PATCH 7/7] fix: review update --- docs/usage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage.md b/docs/usage.md index d6b3e608..4196a296 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -215,7 +215,7 @@ Depending on your environment, you may have to delay updating to the minimum Nod ### Running via container (podman) -The server can also be launched from a container image instead of `npx`. This is useful when you want a pinned, sandboxed runtime that doesn't depend on the host's Node.js installation. Running with an MCP client spawns `podman` (or `docker`) instead of `npx`. +The server can also be launched from a container image instead of `npx`. This is useful when you want a containerized, sandboxed runtime that doesn't depend on the host's Node.js installation. Running with an MCP client spawns `podman` (or `docker`) instead of `npx`. > **Prerequisites:** > `podman` is the supported container runtime for PatternFly MCP. We recommend [podman desktop](https://podman-desktop.io/downloads) or [podman](https://podman.io/).