diff --git a/.github/ghcr-verify-context/Dockerfile b/.github/ghcr-verify-context/Dockerfile new file mode 100644 index 0000000..0eef1a6 --- /dev/null +++ b/.github/ghcr-verify-context/Dockerfile @@ -0,0 +1,3 @@ +# Minimal image to verify Actions can push to ghcr.io/pengine-ai/... +FROM scratch +COPY README.md /README.md diff --git a/.github/scripts/tools-publish/compute-image-tags.sh b/.github/scripts/tools-publish/compute-image-tags.sh deleted file mode 100755 index 81a1f4f..0000000 --- a/.github/scripts/tools-publish/compute-image-tags.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash -# Writes multiline "tags" to GITHUB_OUTPUT for docker/build-push-action. -# Env: IMAGE, VERSION, REF_TYPE (github.ref_type: branch|tag), GITHUB_REF. -set -euo pipefail - -TAGS="${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 - TAGS="${TAGS}"$'\n'"${IMAGE}:latest" -fi -{ - echo 'tags<> "$GITHUB_OUTPUT" diff --git a/.github/scripts/tools-publish/merge-multiarch-manifest.sh b/.github/scripts/tools-publish/merge-multiarch-manifest.sh new file mode 100755 index 0000000..aaf02a8 --- /dev/null +++ b/.github/scripts/tools-publish/merge-multiarch-manifest.sh @@ -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" diff --git a/.github/scripts/tools-publish/write-publish-summary.sh b/.github/scripts/tools-publish/write-publish-summary.sh index e60cef8..c1c480a 100755 --- a/.github/scripts/tools-publish/write-publish-summary.sh +++ b/.github/scripts/tools-publish/write-publish-summary.sh @@ -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" <\` / \`:${TOOL_VERSION}-ci-arm64-\` may appear in the package; **use \`:${TOOL_VERSION}\` or the digest above**—not the \`-ci-*\` tags. EOF diff --git a/.github/workflows/ghcr-verify.yml b/.github/workflows/ghcr-verify.yml new file mode 100644 index 0000000..0d87bc4 --- /dev/null +++ b/.github/workflows/ghcr-verify.yml @@ -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" diff --git a/.github/workflows/tools-publish.yml b/.github/workflows/tools-publish.yml index 2451cc2..e23a503 100644 --- a/.github/workflows/tools-publish.yml +++ b/.github/workflows/tools-publish.yml @@ -5,6 +5,9 @@ name: Publish tool images # tools/mcp-tools.json is the registry — one entry per tool. # tools//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" @@ -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: @@ -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 @@ -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 diff --git a/doc/tool-engine/manual-publish.md b/doc/tool-engine/manual-publish.md index 240e067..5a40b03 100644 --- a/doc/tool-engine/manual-publish.md +++ b/doc/tool-engine/manual-publish.md @@ -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-` | +| Piece | Value | +| --------------- | ----------------------------------- | +| Registry host | `ghcr.io` | +| Repository path | `pengine-ai/pengine-` | -The `` 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 `` 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** @@ -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-` / `:version-ci-arm64-` 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 diff --git a/src-tauri/src/modules/tool_engine/service.rs b/src-tauri/src/modules/tool_engine/service.rs index 1327540..523ea95 100644 --- a/src-tauri/src/modules/tool_engine/service.rs +++ b/src-tauri/src/modules/tool_engine/service.rs @@ -211,8 +211,8 @@ fn resolve_current_digest(entry: &ToolEntry) -> Result, 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 { match resolve_current_digest(entry)? { Some(digest) => Ok(format!("{}@{}", entry.image, digest)), @@ -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() diff --git a/src-tauri/src/modules/tool_engine/tools.json b/src-tauri/src/modules/tool_engine/tools.json index fc9abbd..5153a48 100644 --- a/src-tauri/src/modules/tool_engine/tools.json +++ b/src-tauri/src/modules/tool_engine/tools.json @@ -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/. 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": [ { diff --git a/src-tauri/src/modules/tool_engine/types.rs b/src-tauri/src/modules/tool_engine/types.rs index 924dba9..e033f75 100644 --- a/src-tauri/src/modules/tool_engine/types.rs +++ b/src-tauri/src/modules/tool_engine/types.rs @@ -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, diff --git a/tools/mcp-tools.json b/tools/mcp-tools.json index 5334036..d6ee129 100644 --- a/tools/mcp-tools.json +++ b/tools/mcp-tools.json @@ -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/. 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": [ {