Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/ghcr-verify-context/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Minimal image to verify Actions can push to ghcr.io/pengine-ai/...
FROM scratch
COPY README.md /README.md
24 changes: 0 additions & 24 deletions .github/scripts/tools-publish/compute-image-tags.sh

This file was deleted.

37 changes: 37 additions & 0 deletions .github/scripts/tools-publish/merge-multiarch-manifest.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# Merge two single-arch refs (image@sha256:...) into one multi-arch tag on GHCR.
# Env: IMAGE, VERSION, REF_TYPE, GITHUB_REF, AMD_REF, ARM_REF.
# Appends digest=sha256:... to GITHUB_OUTPUT (manifest list digest for cosign).
set -euo pipefail

AMD="${AMD_REF:?}"
ARM="${ARM_REF:?}"

ARGS=(-t "${IMAGE}:${VERSION}")
LATEST=false
if [[ "${REF_TYPE}" == "branch" && "${GITHUB_REF}" == "refs/heads/main" ]]; then
LATEST=true
elif [[ "${REF_TYPE}" == "tag" ]]; then
T="${GITHUB_REF#refs/tags/}"
T="${T#v}"
if [[ "$T" == "$VERSION" ]]; then
LATEST=true
fi
fi
if [[ "$LATEST" == "true" ]]; then
ARGS+=(-t "${IMAGE}:latest")
fi

docker buildx imagetools create "${ARGS[@]}" "$AMD" "$ARM"

json=$(docker buildx imagetools inspect "${IMAGE}:${VERSION}" --format '{{json .}}' 2>/dev/null || true)
DIGEST=$(echo "$json" | jq -r '.digest // .manifest.digest // .Manifest.Descriptor.Digest // empty' 2>/dev/null || true)
if [[ -z "$DIGEST" || "$DIGEST" == "null" ]]; then
DIGEST=$(docker buildx imagetools inspect "${IMAGE}:${VERSION}" 2>/dev/null | awk '/^[Dd]igest:/ {print $2; exit}')
fi
if [[ -z "$DIGEST" || "$DIGEST" == "null" ]]; then
echo "::error::Could not read manifest list digest from imagetools inspect" >&2
exit 1
fi
echo "digest=$DIGEST" >>"$GITHUB_OUTPUT"
echo "Merged ${IMAGE}:${VERSION} -> $DIGEST"
16 changes: 12 additions & 4 deletions .github/scripts/tools-publish/write-publish-summary.sh
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
#!/usr/bin/env bash
# Appends a job summary section for one published tool.
# Env: TOOL_SLUG, TOOL_VERSION, IMAGE_REF, IMAGE_DIGEST.
# Env: TOOL_SLUG, TOOL_VERSION, IMAGE_REF, IMAGE_DIGEST, RUNNER_ARCH (optional; GitHub runner.arch).
set -euo pipefail

RUNNER_LINE=""
if [[ -n "${RUNNER_ARCH:-}" ]]; then
RUNNER_LINE="- **CI host arch:** \`${RUNNER_ARCH}\` — smoke test pulled the matching layer from the **single** multi-arch manifest below (your machine does the same)."
fi

cat >> "$GITHUB_STEP_SUMMARY" <<EOF
### ${TOOL_SLUG} v${TOOL_VERSION}
- **Image:** \`${IMAGE_REF}\`
- **Platforms:** linux/amd64, linux/arm64
- **Image (multi-arch index):** \`${IMAGE_REF}\`
${RUNNER_LINE}
- **Architectures in manifest:** linux/amd64, linux/arm64
- **Signed:** cosign keyless

Digest for mcp-tools.json:
Digest for mcp-tools.json (one value for all platforms):
\`\`\`
${IMAGE_DIGEST}
\`\`\`

Staging tags \`:${TOOL_VERSION}-ci-amd64-<run>\` / \`:${TOOL_VERSION}-ci-arm64-<run>\` may appear in the package; **use \`:${TOOL_VERSION}\` or the digest above**—not the \`-ci-*\` tags.
EOF
67 changes: 67 additions & 0 deletions .github/workflows/ghcr-verify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Verify GHCR login

# Manual-only: push a tiny image to ghcr.io/... to debug GITHUB_TOKEN + GHCR.

on:
workflow_dispatch:
inputs:
image:
description: 'Full image name without tag (e.g. ghcr.io/pengine-ai/pengine-file-manager)'
required: false
default: 'ghcr.io/pengine-ai/pengine-ghcr-verify'

permissions:
contents: read
packages: write

jobs:
verify:
runs-on: ubuntu-latest
env:
# workflow_dispatch input can be cleared in UI; fall back to default.
VERIFY_IMAGE: ${{ github.event.inputs.image || 'ghcr.io/pengine-ai/pengine-ghcr-verify' }}
steps:
- uses: actions/checkout@v4

- uses: docker/setup-buildx-action@v3

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Push probe image
id: build
uses: docker/build-push-action@v6
with:
context: .github/ghcr-verify-context
file: .github/ghcr-verify-context/Dockerfile
platforms: linux/amd64
push: true
provenance: false
sbom: false
tags: ${{ env.VERIFY_IMAGE }}:verify-${{ github.run_id }}

- name: Pull probe (read check)
env:
REF: ${{ env.VERIFY_IMAGE }}@${{ steps.build.outputs.digest }}
run: |
set -euo pipefail
docker pull "$REF"

- name: Summary
env:
REF: ${{ env.VERIFY_IMAGE }}@${{ steps.build.outputs.digest }}
TAG: ${{ env.VERIFY_IMAGE }}:verify-${{ github.run_id }}
run: |
{
echo '### GHCR verify'
echo "Push succeeded for **\`$TAG\`** (digest below)."
echo "Read check: \`docker pull\` by digest succeeded."
echo ""
echo "**Digest:** \`$REF\`"
echo ""
echo 'You can delete the probe tag in **Packages** when finished.'
} >> "$GITHUB_STEP_SUMMARY"
60 changes: 42 additions & 18 deletions .github/workflows/tools-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ name: Publish tool images
# tools/mcp-tools.json is the registry — one entry per tool.
# tools/<slug>/Dockerfile is the build context.
#
# Two build-push steps (amd64, arm64) on ubuntu-24.04-arm, then one merge step
# so GHCR and the job summary show a single multi-arch digest (not two products).
#
# Triggers:
# - Push to main touching tools/** → build only changed tools
# - Manual dispatch → pick one slug or "all"
Expand Down Expand Up @@ -32,7 +35,7 @@ jobs:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.detect.outputs.matrix }}
skip: ${{ steps.detect.outputs.skip }}
skip: ${{ steps.detect.outputs.skip }}
steps:
- uses: actions/checkout@v4
with:
Expand All @@ -47,18 +50,19 @@ jobs:
publish:
needs: detect
if: needs.detect.outputs.skip == 'false'
runs-on: ubuntu-latest
runs-on: ubuntu-24.04-arm
strategy:
fail-fast: false
matrix:
slug: ${{ fromJson(needs.detect.outputs.matrix) }}
steps:
- uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: sigstore/cosign-installer@v3

Expand All @@ -68,38 +72,58 @@ jobs:
TOOL_SLUG: ${{ matrix.slug }}
run: bash .github/scripts/tools-publish/read-tool-manifest.sh

- name: Compute image tags
id: img_tags
env:
IMAGE: ${{ steps.cfg.outputs.image }}
VERSION: ${{ steps.cfg.outputs.version }}
REF_TYPE: ${{ github.ref_type }}
run: bash .github/scripts/tools-publish/compute-image-tags.sh
- name: Build and push (linux/arm64)
id: build_arm64
uses: docker/build-push-action@v6
with:
context: tools/${{ matrix.slug }}/
platforms: linux/arm64
push: true
provenance: false
sbom: false
build-args: |
UPSTREAM_MCP_NPM_PACKAGE=${{ steps.cfg.outputs.npm_pkg }}
UPSTREAM_MCP_NPM_VERSION=${{ steps.cfg.outputs.npm_ver }}
tags: ${{ steps.cfg.outputs.image }}:${{ steps.cfg.outputs.version }}-ci-arm64-${{ github.run_id }}

- name: Build and push
id: build
- name: Build and push (linux/amd64)
id: build_amd64
uses: docker/build-push-action@v6
with:
context: tools/${{ matrix.slug }}/
platforms: linux/amd64,linux/arm64
platforms: linux/amd64
push: true
provenance: false
sbom: false
build-args: |
UPSTREAM_MCP_NPM_PACKAGE=${{ steps.cfg.outputs.npm_pkg }}
UPSTREAM_MCP_NPM_VERSION=${{ steps.cfg.outputs.npm_ver }}
tags: ${{ steps.img_tags.outputs.tags }}
tags: ${{ steps.cfg.outputs.image }}:${{ steps.cfg.outputs.version }}-ci-amd64-${{ github.run_id }}

- name: Merge multi-arch manifest
id: merge
env:
IMAGE: ${{ steps.cfg.outputs.image }}
VERSION: ${{ steps.cfg.outputs.version }}
REF_TYPE: ${{ github.ref_type }}
GITHUB_REF: ${{ github.ref }}
AMD_REF: ${{ steps.cfg.outputs.image }}@${{ steps.build_amd64.outputs.digest }}
ARM_REF: ${{ steps.cfg.outputs.image }}@${{ steps.build_arm64.outputs.digest }}
run: bash .github/scripts/tools-publish/merge-multiarch-manifest.sh

- name: Sign image
run: cosign sign --yes "${{ steps.cfg.outputs.image }}@${{ steps.build.outputs.digest }}"
run: cosign sign --yes "${{ steps.cfg.outputs.image }}@${{ steps.merge.outputs.digest }}"

- name: Smoke test (MCP init handshake)
env:
IMAGE_WITH_DIGEST: ${{ steps.cfg.outputs.image }}@${{ steps.build.outputs.digest }}
IMAGE_WITH_DIGEST: ${{ steps.cfg.outputs.image }}@${{ steps.merge.outputs.digest }}
run: bash .github/scripts/tools-publish/smoke-test-mcp.sh

- name: Summary
env:
TOOL_SLUG: ${{ matrix.slug }}
TOOL_VERSION: ${{ steps.cfg.outputs.version }}
IMAGE_REF: ${{ steps.cfg.outputs.image }}@${{ steps.build.outputs.digest }}
IMAGE_DIGEST: ${{ steps.build.outputs.digest }}
IMAGE_REF: ${{ steps.cfg.outputs.image }}@${{ steps.merge.outputs.digest }}
IMAGE_DIGEST: ${{ steps.merge.outputs.digest }}
RUNNER_ARCH: ${{ runner.arch }}
run: bash .github/scripts/tools-publish/write-publish-summary.sh
16 changes: 9 additions & 7 deletions doc/tool-engine/manual-publish.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ Tool container images for Pengine live on **GitHub Container Registry (GHCR)**.

## Where the images are

| Piece | Value |
|--------|--------|
| Registry host | `ghcr.io` |
| Repository path | `pengine-ai/tools/pengine-<suffix>` |
| Piece | Value |
| --------------- | ----------------------------------- |
| Registry host | `ghcr.io` |
| Repository path | `pengine-ai/pengine-<suffix>` |

The `<suffix>` comes from the tool `id` in `tools/mcp-tools.json`. Example: id `pengine/file-manager` → image `ghcr.io/pengine-ai/tools/pengine-file-manager`.
The `<suffix>` comes from the tool `id` in `tools/mcp-tools.json`. Example: id `pengine/file-manager` → image `ghcr.io/pengine-ai/pengine-file-manager`.

Pull examples:

```bash
podman pull ghcr.io/pengine-ai/tools/pengine-file-manager:0.1.0
podman pull ghcr.io/pengine-ai/tools/pengine-file-manager:latest
podman pull ghcr.io/pengine-ai/pengine-file-manager:0.1.0
podman pull ghcr.io/pengine-ai/pengine-file-manager:latest
```

Browse packages on GitHub: **https://github.com/orgs/pengine-ai/packages**
Expand Down Expand Up @@ -103,6 +103,8 @@ This checks the npm registry for newer versions, bumps `mcp-tools.json`, and pri

Or just push changes to `tools/` on `main` — CI builds automatically for tools whose version changed.

CI runs **one job per tool** on **`ubuntu-24.04-arm`**: two `docker/build-push-action` steps (native **linux/arm64**, then **linux/amd64** via QEMU), then **`docker buildx imagetools create`** so **one** multi-arch tag (`:version` / optional `:latest`) and **one digest** appear in the job summary. Staging tags `:version-ci-amd64-<run>` / `:version-ci-arm64-<run>` may still show in the GitHub Packages UI until you delete them; use `:version` or the digest from the summary for production.

---

## Troubleshooting: 0 commands
Expand Down
6 changes: 3 additions & 3 deletions src-tauri/src/modules/tool_engine/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,8 @@ fn resolve_current_digest(entry: &ToolEntry) -> Result<Option<String>, String> {

/// The OCI image reference for a tool entry.
///
/// - **Production** (real digest): `ghcr.io/pengine-ai/tools/pengine-file-manager@sha256:abc123…`
/// - **Dev** (placeholder digest): `ghcr.io/pengine-ai/tools/pengine-file-manager:0.1.0` (tagged)
/// - **Production** (real digest): `ghcr.io/pengine-ai/pengine-file-manager@sha256:abc123…`
/// - **Dev** (placeholder digest): `ghcr.io/pengine-ai/pengine-file-manager:0.1.0` (tagged)
fn image_reference(entry: &ToolEntry) -> Result<String, String> {
match resolve_current_digest(entry)? {
Some(digest) => Ok(format!("{}@{}", entry.image, digest)),
Expand Down Expand Up @@ -654,7 +654,7 @@ mod tests {
.expect("file-manager must be in embedded catalog");
assert_eq!(fm.current, "0.1.0");
assert!(!fm.versions.is_empty());
assert!(fm.image.contains("ghcr.io/pengine-ai/tools/"));
assert!(fm.image.contains("ghcr.io/pengine-ai/pengine-file-manager"));
let u = fm
.upstream_mcp_npm
.as_ref()
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/src/modules/tool_engine/tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"id": "pengine/file-manager",
"name": "File Manager",
"description": "Filesystem MCP in a container. Add folders in MCP Tools; each mounts at /app/<folder-name>. Install works before any folder is set.",
"image": "ghcr.io/pengine-ai/tools/pengine-file-manager",
"image": "ghcr.io/pengine-ai/pengine-file-manager",
"current": "0.1.0",
"versions": [
{
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/src/modules/tool_engine/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ pub struct ToolEntry {
pub id: String,
pub name: String,
pub description: String,
/// Full OCI image reference (without tag/digest), e.g. "ghcr.io/pengine-ai/tools/pengine-file-manager".
/// Full OCI image reference (without tag/digest), e.g. "ghcr.io/pengine-ai/pengine-file-manager".
pub image: String,
/// The current (latest non-yanked, non-revoked) version string, e.g. "0.1.0".
pub current: String,
Expand Down
2 changes: 1 addition & 1 deletion tools/mcp-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"id": "pengine/file-manager",
"name": "File Manager",
"description": "Filesystem MCP in a container. Add folders in MCP Tools; each mounts at /app/<folder-name>. Install works before any folder is set.",
"image": "ghcr.io/pengine-ai/tools/pengine-file-manager",
"image": "ghcr.io/pengine-ai/pengine-file-manager",
"current": "0.1.0",
"versions": [
{
Expand Down
Loading