Skip to content
Closed
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
5 changes: 0 additions & 5 deletions .github/aw/actions-lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -235,11 +235,6 @@
"digest": "sha256:9cb5f26d30b609a1d5560a090371cb9dba84fc32e7df10e5496aa22aa7b52658",
"pinned_image": "mcp/markitdown@sha256:9cb5f26d30b609a1d5560a090371cb9dba84fc32e7df10e5496aa22aa7b52658"
},
"mcp/memory": {
"image": "mcp/memory",
"digest": "sha256:db0c2db07a44b6797eba7a832b1bda142ffc899588aae82c92780cbb2252407f",
"pinned_image": "mcp/memory@sha256:db0c2db07a44b6797eba7a832b1bda142ffc899588aae82c92780cbb2252407f"
},
"mcp/notion": {
"image": "mcp/notion",
"digest": "sha256:df0d6781d03f37bd5b962c85ae1f288382f31b7108c489473641ffc372f43dc9",
Expand Down
14 changes: 7 additions & 7 deletions .github/workflows/mcp-inspector.lock.yml

Large diffs are not rendered by default.

276 changes: 276 additions & 0 deletions .github/workflows/publish-memory-mcp.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
name: Publish memory MCP image

on:
workflow_dispatch:
schedule:
- cron: '41 4 * * 1'

permissions:
contents: read
packages: write

concurrency:
group: publish-memory-mcp
cancel-in-progress: false

env:
IMAGE_NAME: ghcr.io/github/gh-aw-memory
NODE_IMAGE_TAG: node:lts-alpine
NODE_IMAGE_METADATA_URL: https://hub.docker.com/v2/repositories/library/node/tags/lts-alpine
NODE_IMAGE_COOLDOWN_DAYS: '1'
MCP_MEMORY_VERSION: '2026.7.4'

jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7.0.0
with:
persist-credentials: false

- name: Resolve upstream Node image metadata
id: upstream
env:
COOLDOWN_DAYS: ${{ env.NODE_IMAGE_COOLDOWN_DAYS }}
METADATA_URL: ${{ env.NODE_IMAGE_METADATA_URL }}
NODE_IMAGE_TAG: ${{ env.NODE_IMAGE_TAG }}
run: |
set -euo pipefail

echo "Fetching metadata for ${NODE_IMAGE_TAG} from ${METADATA_URL}"
metadata=$(curl -fsSL "$METADATA_URL")
echo "Metadata fetched successfully"

digest=$(printf '%s' "$metadata" | jq -r '.digest')
amd64_digest=$(printf '%s' "$metadata" | jq -r '.images[] | select(.os == "linux" and .architecture == "amd64") | .digest' | head -n 1)
arm64_digest=$(printf '%s' "$metadata" | jq -r '.images[] | select(.os == "linux" and .architecture == "arm64") | .digest' | head -n 1)
last_updated=$(printf '%s' "$metadata" | jq -r '.last_updated')

echo "Resolved multi-platform digest: ${digest}"
echo "Resolved linux/amd64 digest: ${amd64_digest}"
echo "Resolved linux/arm64 digest: ${arm64_digest}"
echo "Upstream last_updated: ${last_updated}"

if [ -z "$digest" ] || [ "$digest" = "null" ] \
|| [ -z "$amd64_digest" ] || [ "$amd64_digest" = "null" ] \
|| [ -z "$arm64_digest" ] || [ "$arm64_digest" = "null" ] \
|| [ -z "$last_updated" ] || [ "$last_updated" = "null" ]; then
echo "Failed to resolve node:lts-alpine metadata"
exit 1
fi

last_updated_epoch=$(date -u -d "$last_updated" +%s)
now_epoch=$(date -u +%s)
cooldown_seconds=$((COOLDOWN_DAYS * 24 * 60 * 60))
age_seconds=$((now_epoch - last_updated_epoch))
if [ "$age_seconds" -lt 0 ]; then
age_seconds=0
fi

in_cooldown=false
if [ "$age_seconds" -lt "$cooldown_seconds" ]; then
in_cooldown=true
fi

short_digest=$(printf '%s' "$digest" | sed 's/^sha256://' | cut -c1-12)

echo "Upstream image age: $((age_seconds / 86400)) day(s) (cooldown window: ${COOLDOWN_DAYS} day(s))"
echo "In cooldown: ${in_cooldown}"
echo "Short digest: ${short_digest}"

echo "base_image=${NODE_IMAGE_TAG}@${digest}" >> "$GITHUB_OUTPUT"
echo "digest=$digest" >> "$GITHUB_OUTPUT"
echo "short_digest=$short_digest" >> "$GITHUB_OUTPUT"
echo "last_updated=$last_updated" >> "$GITHUB_OUTPUT"
echo "age_days=$((age_seconds / 86400))" >> "$GITHUB_OUTPUT"
echo "in_cooldown=$in_cooldown" >> "$GITHUB_OUTPUT"

- name: Compute container source hash
id: source
run: |
set -euo pipefail

dockerfile="actions/setup/js/Dockerfile.memory-mcp"
echo "Computing SHA-256 hash of ${dockerfile}"
hash=$(sha256sum "$dockerfile" | cut -d' ' -f1)
echo "Dockerfile hash: ${hash}"

echo "hash=$hash" >> "$GITHUB_OUTPUT"

- name: Log in to GitHub Container Registry
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Inspect published image
id: published
env:
IMAGE_NAME: ${{ env.IMAGE_NAME }}
run: |
set -euo pipefail

image_exists=false
existing_digest=""
existing_revision=""
existing_dockerfile_hash=""
existing_mcp_memory_version=""

echo "Attempting to pull ${IMAGE_NAME}:latest"
if docker pull "$IMAGE_NAME:latest" >/dev/null 2>&1; then
image_exists=true
echo "Image pulled successfully; extracting labels"
existing_digest=$(docker inspect "$IMAGE_NAME:latest" --format '{{ index .Config.Labels "org.opencontainers.image.base.digest" }}' 2>/dev/null || true)
existing_revision=$(docker inspect "$IMAGE_NAME:latest" --format '{{ index .Config.Labels "org.opencontainers.image.revision" }}' 2>/dev/null || true)
existing_dockerfile_hash=$(docker inspect "$IMAGE_NAME:latest" --format '{{ index .Config.Labels "io.github.gh-aw.memory.dockerfile-hash" }}' 2>/dev/null || true)
existing_mcp_memory_version=$(docker inspect "$IMAGE_NAME:latest" --format '{{ index .Config.Labels "io.github.gh-aw.memory.server-memory.version" }}' 2>/dev/null || true)
else
echo "Image ${IMAGE_NAME}:latest not found in registry"
fi

if [ "$existing_digest" = "<no value>" ]; then
existing_digest=""
fi
if [ "$existing_revision" = "<no value>" ]; then
existing_revision=""
fi
if [ "$existing_dockerfile_hash" = "<no value>" ]; then
existing_dockerfile_hash=""
fi
if [ "$existing_mcp_memory_version" = "<no value>" ]; then
existing_mcp_memory_version=""
fi

echo "Image exists: ${image_exists}"
echo "Published base digest: ${existing_digest:-none}"
echo "Published revision: ${existing_revision:-none}"
echo "Published Dockerfile hash: ${existing_dockerfile_hash:-none}"
echo "Published server-memory version: ${existing_mcp_memory_version:-none}"

echo "image_exists=$image_exists" >> "$GITHUB_OUTPUT"
echo "base_digest=$existing_digest" >> "$GITHUB_OUTPUT"
echo "revision=$existing_revision" >> "$GITHUB_OUTPUT"
echo "dockerfile_hash=$existing_dockerfile_hash" >> "$GITHUB_OUTPUT"
echo "mcp_memory_version=$existing_mcp_memory_version" >> "$GITHUB_OUTPUT"

- name: Decide whether to publish
id: decision
env:
COOLDOWN_DAYS: ${{ env.NODE_IMAGE_COOLDOWN_DAYS }}
CURRENT_BASE_DIGEST: ${{ steps.upstream.outputs.digest }}
CURRENT_DOCKERFILE_HASH: ${{ steps.source.outputs.hash }}
CURRENT_MCP_MEMORY_VERSION: ${{ env.MCP_MEMORY_VERSION }}
EXISTING_BASE_DIGEST: ${{ steps.published.outputs.base_digest }}
EXISTING_DOCKERFILE_HASH: ${{ steps.published.outputs.dockerfile_hash }}
EXISTING_MCP_MEMORY_VERSION: ${{ steps.published.outputs.mcp_memory_version }}
IMAGE_EXISTS: ${{ steps.published.outputs.image_exists }}
IN_COOLDOWN: ${{ steps.upstream.outputs.in_cooldown }}
run: |
set -euo pipefail

echo "--- Decision inputs ---"
echo "Image exists: ${IMAGE_EXISTS}"
echo "In cooldown: ${IN_COOLDOWN} (window: ${COOLDOWN_DAYS} day(s))"
echo "Current upstream digest: ${CURRENT_BASE_DIGEST}"
echo "Published base digest: ${EXISTING_BASE_DIGEST:-none}"
echo "Current Dockerfile hash: ${CURRENT_DOCKERFILE_HASH}"
echo "Published Dockerfile hash: ${EXISTING_DOCKERFILE_HASH:-none}"
echo "Current server-memory version: ${CURRENT_MCP_MEMORY_VERSION}"
echo "Published server-memory version: ${EXISTING_MCP_MEMORY_VERSION:-none}"
echo "--- Evaluating conditions ---"

should_push=false
reason="No changes detected"

if [ "$IN_COOLDOWN" = "true" ] && { [ "$IMAGE_EXISTS" != "true" ] || [ "$EXISTING_BASE_DIGEST" != "$CURRENT_BASE_DIGEST" ]; }; then
reason="node:lts-alpine is still within the ${COOLDOWN_DAYS}-day cooldown window"
elif [ "$IMAGE_EXISTS" != "true" ]; then
should_push=true
reason="Published image does not exist"
elif [ "$EXISTING_BASE_DIGEST" != "$CURRENT_BASE_DIGEST" ]; then
should_push=true
reason="Upstream node:lts-alpine digest changed"
elif [ "$EXISTING_DOCKERFILE_HASH" != "$CURRENT_DOCKERFILE_HASH" ]; then
should_push=true
reason="Container Dockerfile changed"
elif [ "$EXISTING_MCP_MEMORY_VERSION" != "$CURRENT_MCP_MEMORY_VERSION" ]; then
should_push=true
reason="server-memory version changed to ${CURRENT_MCP_MEMORY_VERSION}"
fi

echo "Decision: should_push=${should_push}"
echo "Reason: ${reason}"

echo "should_push=$should_push" >> "$GITHUB_OUTPUT"
echo "reason=$reason" >> "$GITHUB_OUTPUT"

- name: Set up QEMU
if: steps.decision.outputs.should_push == 'true'
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0

- name: Set up Docker Buildx
if: steps.decision.outputs.should_push == 'true'
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0

- name: Build and push gh-aw-memory
if: steps.decision.outputs.should_push == 'true'
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
file: actions/setup/js/Dockerfile.memory-mcp
platforms: linux/amd64,linux/arm64
pull: true
push: true
build-args: |
NODE_IMAGE=${{ steps.upstream.outputs.base_image }}
NODE_IMAGE_DIGEST=${{ steps.upstream.outputs.digest }}
NODE_IMAGE_UPDATED_AT=${{ steps.upstream.outputs.last_updated }}
DOCKERFILE_HASH=${{ steps.source.outputs.hash }}
MCP_MEMORY_VERSION=${{ env.MCP_MEMORY_VERSION }}
tags: |
${{ env.IMAGE_NAME }}:latest
${{ env.IMAGE_NAME }}:node-lts-alpine-${{ steps.upstream.outputs.short_digest }}
labels: |
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
org.opencontainers.image.description=Hardened MCP memory server built from node:lts-alpine with patched dependencies
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.base.name=${{ env.NODE_IMAGE_TAG }}
org.opencontainers.image.base.digest=${{ steps.upstream.outputs.digest }}
io.github.gh-aw.memory.node.updated-at=${{ steps.upstream.outputs.last_updated }}
io.github.gh-aw.memory.dockerfile-hash=${{ steps.source.outputs.hash }}
io.github.gh-aw.memory.server-memory.version=${{ env.MCP_MEMORY_VERSION }}
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Write summary
if: always()
env:
AGE_DAYS: ${{ steps.upstream.outputs.age_days }}
BASE_DIGEST: ${{ steps.upstream.outputs.digest }}
EXISTING_BASE_DIGEST: ${{ steps.published.outputs.base_digest }}
EXISTING_DOCKERFILE_HASH: ${{ steps.published.outputs.dockerfile_hash }}
IMAGE_EXISTS: ${{ steps.published.outputs.image_exists }}
IN_COOLDOWN: ${{ steps.upstream.outputs.in_cooldown }}
MCP_MEMORY_VERSION: ${{ env.MCP_MEMORY_VERSION }}
REASON: ${{ steps.decision.outputs.reason }}
DOCKERFILE_HASH: ${{ steps.source.outputs.hash }}
SHOULD_PUSH: ${{ steps.decision.outputs.should_push }}
UPDATED_AT: ${{ steps.upstream.outputs.last_updated }}
run: |
{
echo "## gh-aw-memory publish decision"
echo ""
echo "- Published image exists: ${IMAGE_EXISTS}"
echo "- Current node:lts-alpine digest: \`${BASE_DIGEST}\`"
echo "- Published base digest: \`${EXISTING_BASE_DIGEST:-none}\`"
echo "- Current Dockerfile hash: \`${DOCKERFILE_HASH}\`"
echo "- Published Dockerfile hash: \`${EXISTING_DOCKERFILE_HASH:-none}\`"
echo "- server-memory version: \`${MCP_MEMORY_VERSION}\`"
echo "- Upstream last updated: ${UPDATED_AT}"
echo "- Upstream age: ${AGE_DAYS} day(s)"
echo "- In cooldown: ${IN_COOLDOWN}"
echo "- Push image: ${SHOULD_PUSH}"
echo "- Reason: ${REASON}"
} >> "$GITHUB_STEP_SUMMARY"
2 changes: 1 addition & 1 deletion .github/workflows/shared/mcp/server-memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

mcp-servers:
memory:
container: "mcp/memory"
container: "ghcr.io/github/gh-aw-memory"
args:
- "-v"
- "/tmp/gh-aw/cache-memory:/app/dist"
Expand Down
45 changes: 45 additions & 0 deletions actions/setup/js/Dockerfile.memory-mcp
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# syntax=docker/dockerfile:1.7

ARG NODE_IMAGE=node:lts-alpine
FROM ${NODE_IMAGE}

ARG NODE_IMAGE
ARG NODE_IMAGE_DIGEST=""
ARG NODE_IMAGE_UPDATED_AT=""
ARG DOCKERFILE_HASH=""
ARG NPM_VERSION=11.19.0
ARG MCP_MEMORY_VERSION=2026.7.4

WORKDIR /app

# Intentional: upgrade all packages to pick up security fixes; downstream digest pins the result.
# After upgrading npm, patch its bundled dependencies to meet minimum safe versions
# (tar >= 7.5.22 for GHSA-23hp-3jrh-7fpw, brace-expansion >= 5.0.8 for GHSA-mh99-v99m-4gvg).
RUN apk upgrade --no-cache \
&& apk info -v | sort \
&& npm install --global "npm@${NPM_VERSION}" \
&& npm install --prefix "$(npm root -g)/npm" --no-save "tar@^7.5.22" "brace-expansion@^5.0.8" \
&& npm cache clean --force

# Install @modelcontextprotocol/server-memory from npm.
# Using a pinned version ensures the image is reproducible and picks up
# a fully-patched @modelcontextprotocol/sdk (>= 1.24.0).
RUN npm install "@modelcontextprotocol/server-memory@${MCP_MEMORY_VERSION}" \
&& npm cache clean --force

# Persist memory to /app/dist/ so the existing Docker volume mount
# (-v /tmp/gh-aw/cache-memory:/app/dist) continues to work.
RUN mkdir -p /app/dist
ENV MEMORY_FILE_PATH=/app/dist/memory.json
ENV NODE_ENV=production

LABEL org.opencontainers.image.source="https://github.com/github/gh-aw" \
org.opencontainers.image.description="Hardened MCP memory server built from node:lts-alpine with patched dependencies" \
org.opencontainers.image.base.name="${NODE_IMAGE}" \
org.opencontainers.image.base.digest="${NODE_IMAGE_DIGEST}" \
io.github.gh-aw.memory.node.updated-at="${NODE_IMAGE_UPDATED_AT}" \
io.github.gh-aw.memory.npm.version="${NPM_VERSION}" \
io.github.gh-aw.memory.server-memory.version="${MCP_MEMORY_VERSION}" \
io.github.gh-aw.memory.dockerfile-hash="${DOCKERFILE_HASH}"

ENTRYPOINT ["node", "node_modules/@modelcontextprotocol/server-memory/dist/index.js"]
5 changes: 0 additions & 5 deletions pkg/actionpins/data/action_pins.json
Original file line number Diff line number Diff line change
Expand Up @@ -235,11 +235,6 @@
"digest": "sha256:9cb5f26d30b609a1d5560a090371cb9dba84fc32e7df10e5496aa22aa7b52658",
"pinned_image": "mcp/markitdown@sha256:9cb5f26d30b609a1d5560a090371cb9dba84fc32e7df10e5496aa22aa7b52658"
},
"mcp/memory": {
"image": "mcp/memory",
"digest": "sha256:db0c2db07a44b6797eba7a832b1bda142ffc899588aae82c92780cbb2252407f",
"pinned_image": "mcp/memory@sha256:db0c2db07a44b6797eba7a832b1bda142ffc899588aae82c92780cbb2252407f"
},
"mcp/notion": {
"image": "mcp/notion",
"digest": "sha256:df0d6781d03f37bd5b962c85ae1f288382f31b7108c489473641ffc372f43dc9",
Expand Down
5 changes: 0 additions & 5 deletions pkg/workflow/data/action_pins.json
Original file line number Diff line number Diff line change
Expand Up @@ -235,11 +235,6 @@
"digest": "sha256:9cb5f26d30b609a1d5560a090371cb9dba84fc32e7df10e5496aa22aa7b52658",
"pinned_image": "mcp/markitdown@sha256:9cb5f26d30b609a1d5560a090371cb9dba84fc32e7df10e5496aa22aa7b52658"
},
"mcp/memory": {
"image": "mcp/memory",
"digest": "sha256:db0c2db07a44b6797eba7a832b1bda142ffc899588aae82c92780cbb2252407f",
"pinned_image": "mcp/memory@sha256:db0c2db07a44b6797eba7a832b1bda142ffc899588aae82c92780cbb2252407f"
},
"mcp/notion": {
"image": "mcp/notion",
"digest": "sha256:df0d6781d03f37bd5b962c85ae1f288382f31b7108c489473641ffc372f43dc9",
Expand Down