Skip to content

core-npm-published

core-npm-published #3

name: Publish Core Docker Image
on:
repository_dispatch:
types:
- core-npm-published
permissions:
contents: read
packages: write
concurrency:
group: publish-core-docker-${{ github.event.client_payload.core_version }}
cancel-in-progress: false
defaults:
run:
shell: bash
env:
IMAGE_NAME: ghcr.io/atomicstrata/atomicmemory-core
jobs:
publish:
name: publish @atomicmemory/core Docker image
runs-on: ubuntu-24.04
timeout-minutes: 60
steps:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "22"
- name: Login to GHCR
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: echo "${GITHUB_TOKEN}" | docker login ghcr.io -u "${GITHUB_ACTOR}" --password-stdin
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130
with:
platforms: arm64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f
- name: Resolve published package source
id: package
env:
CORE_VERSION_INPUT: ${{ github.event.client_payload.core_version }}
IMAGE_NAME: ${{ env.IMAGE_NAME }}
run: |
set -euo pipefail
if [[ -z "${CORE_VERSION_INPUT}" ]]; then
echo "::error::core_version is required for repository_dispatch or workflow_dispatch."
exit 1
fi
if ! metadata="$(npm view "@atomicmemory/core@${CORE_VERSION_INPUT}" version gitHead dist.tarball --json 2>/tmp/core-npm-view.err)"; then
cat /tmp/core-npm-view.err >&2
exit 1
fi
version="$(jq -r '.version' <<<"${metadata}")"
git_head="$(jq -r '.gitHead' <<<"${metadata}")"
short_sha="${git_head:0:7}"
tarball="$(jq -r '.dist.tarball' <<<"${metadata}")"
latest_version="$(npm view @atomicmemory/core@latest version)"
if [[ -z "${version}" || "${version}" == "null" ]]; then
echo "::error::Could not resolve @atomicmemory/core@${CORE_VERSION_INPUT}"
exit 1
fi
if [[ -z "${git_head}" || "${git_head}" == "null" ]]; then
echo "::error::@atomicmemory/core@${version} does not include npm gitHead metadata"
exit 1
fi
tag_exists() {
docker manifest inspect "$1" >/dev/null 2>&1
}
tag_has_platform() {
local image_ref="$1"
local os="$2"
local arch="$3"
docker manifest inspect "${image_ref}" --verbose 2>/dev/null | jq -e --arg os "${os}" --arg arch "${arch}" '
if type == "array" then
any(.[]; .Descriptor.platform.os == $os and .Descriptor.platform.architecture == $arch)
else
.Descriptor.platform.os == $os and .Descriptor.platform.architecture == $arch
end
' >/dev/null
}
tag_has_required_platforms() {
tag_has_platform "$1" linux amd64 && tag_has_platform "$1" linux arm64
}
is_latest="$([[ "${version}" == "${latest_version}" ]] && echo true || echo false)"
release_refs=(
"${IMAGE_NAME}:${version}"
"${IMAGE_NAME}:${short_sha}"
"${IMAGE_NAME}:sha-${short_sha}"
)
missing_platform_refs=()
for image_ref in "${release_refs[@]}"; do
if ! tag_exists "${image_ref}" || ! tag_has_required_platforms "${image_ref}"; then
missing_platform_refs+=("${image_ref}")
fi
done
release_tags_have_required_platforms=false
if [[ "${#missing_platform_refs[@]}" == "0" ]]; then
release_tags_have_required_platforms=true
fi
if [[ "${release_tags_have_required_platforms}" == "true" && "${is_latest}" == "true" ]]; then
{
echo "should_publish=true"
echo "retag_latest_only=true"
echo "version=${version}"
echo "git_head=${git_head}"
echo "short_sha=${short_sha}"
echo "tarball=${tarball}"
echo "is_latest=true"
} >>"${GITHUB_OUTPUT}"
echo "Release tags already include linux/amd64 and linux/arm64; ensuring latest points to ${IMAGE_NAME}:${version}."
exit 0
fi
if [[ "${release_tags_have_required_platforms}" == "true" ]]; then
echo "should_publish=false" >>"${GITHUB_OUTPUT}"
echo "skip_reason=Release tags already include linux/amd64 and linux/arm64." >>"${GITHUB_OUTPUT}"
echo "Release tags already include linux/amd64 and linux/arm64; no Docker publish required."
exit 0
fi
{
echo "should_publish=true"
echo "retag_latest_only=false"
echo "version=${version}"
echo "git_head=${git_head}"
echo "short_sha=${short_sha}"
echo "tarball=${tarball}"
echo "is_latest=${is_latest}"
} >>"${GITHUB_OUTPUT}"
echo "Resolved @atomicmemory/core@${version}"
echo "gitHead=${git_head}"
echo "tarball=${tarball}"
printf 'Rebuilding release tags without complete linux/amd64 and linux/arm64 coverage:\n'
printf ' - %s\n' "${missing_platform_refs[@]}"
- name: Report skipped publish
if: steps.package.outputs.should_publish != 'true'
run: echo "${{ steps.package.outputs.skip_reason }}"
- name: Checkout package gitHead
if: steps.package.outputs.should_publish == 'true' && steps.package.outputs.retag_latest_only != 'true'
uses: actions/checkout@v4
with:
ref: ${{ steps.package.outputs.git_head }}
path: release-source
- name: Verify checked-out package version
if: steps.package.outputs.should_publish == 'true' && steps.package.outputs.retag_latest_only != 'true'
working-directory: release-source
run: |
set -euo pipefail
checked_out_version="$(node -p "require('./packages/core/package.json').version")"
if [[ "${checked_out_version}" != "${{ steps.package.outputs.version }}" ]]; then
echo "::error::packages/core/package.json is ${checked_out_version}, expected ${{ steps.package.outputs.version }}"
exit 1
fi
- name: Build local release image
if: steps.package.outputs.should_publish == 'true' && steps.package.outputs.retag_latest_only != 'true'
run: |
set -euo pipefail
docker build \
--platform linux/amd64 \
--file release-source/packages/core/Dockerfile \
--label "org.opencontainers.image.source=https://github.com/${GITHUB_REPOSITORY}" \
--label "org.opencontainers.image.revision=${{ steps.package.outputs.git_head }}" \
--label "org.opencontainers.image.version=${{ steps.package.outputs.version }}" \
--label "org.opencontainers.image.title=@atomicmemory/core" \
--tag "${IMAGE_NAME}:${{ steps.package.outputs.version }}" \
--tag "${IMAGE_NAME}:${{ steps.package.outputs.short_sha }}" \
--tag "${IMAGE_NAME}:sha-${{ steps.package.outputs.short_sha }}" \
release-source
if [[ "${{ steps.package.outputs.is_latest }}" == "true" ]]; then
docker tag "${IMAGE_NAME}:${{ steps.package.outputs.version }}" "${IMAGE_NAME}:latest"
fi
- name: Verify image package version
if: steps.package.outputs.should_publish == 'true' && steps.package.outputs.retag_latest_only != 'true'
run: |
set -euo pipefail
image_version="$(docker run --rm --entrypoint node "${IMAGE_NAME}:${{ steps.package.outputs.version }}" -p "require('./package.json').version")"
if [[ "${image_version}" != "${{ steps.package.outputs.version }}" ]]; then
echo "::error::Built image package version is ${image_version}, expected ${{ steps.package.outputs.version }}"
exit 1
fi
- name: Smoke test local release image
if: steps.package.outputs.should_publish == 'true' && steps.package.outputs.retag_latest_only != 'true'
env:
CORE_IMAGE: ${{ env.IMAGE_NAME }}:${{ steps.package.outputs.version }}
run: |
set -euo pipefail
smoke_dir="${RUNNER_TEMP}/core-docker-smoke"
compose_project="atomicmemory-core-release-${{ steps.package.outputs.short_sha }}"
app_port="3060"
postgres_port="5444"
core_api_key="test-core-api-key-do-not-leak"
storage_secret="000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"
rm -rf "${smoke_dir}"
mkdir -p "${smoke_dir}"
cd "${smoke_dir}"
cat > .env <<EOF
CORE_API_KEY=${core_api_key}
STORAGE_KEY_HMAC_SECRET=${storage_secret}
RAW_STORAGE_DEPLOYMENT_ENV=local
EOF
cat > docker-compose.yml <<EOF
services:
postgres:
image: pgvector/pgvector:pg17
environment:
POSTGRES_USER: atomicmemory
POSTGRES_PASSWORD: atomicmemory
POSTGRES_DB: atomicmemory
ports:
- "${postgres_port}:5432"
healthcheck:
test: ["CMD", "pg_isready", "-q", "-d", "atomicmemory", "-U", "atomicmemory"]
interval: 5s
timeout: 5s
retries: 5
app:
image: ${CORE_IMAGE}
ports:
- "${app_port}:17350"
environment:
DATABASE_URL: postgresql://atomicmemory:atomicmemory@postgres:5432/atomicmemory
PORT: "17350"
EMBEDDING_PROVIDER: transformers
EMBEDDING_MODEL: Xenova/all-MiniLM-L6-v2
EMBEDDING_DIMENSIONS: "384"
LLM_PROVIDER: openai
OPENAI_API_KEY: sk-smoke-test-dummy
env_file:
- .env
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://localhost:17350/health').then(r=>{if(!r.ok)throw 1}).catch(()=>process.exit(1))"]
interval: 15s
timeout: 5s
retries: 3
start_period: 30s
EOF
cleanup() {
docker compose -p "${compose_project}" down -v --remove-orphans || true
}
trap cleanup EXIT
docker compose -p "${compose_project}" up -d
for attempt in {1..45}; do
if curl -fsS "http://localhost:${app_port}/health" >/dev/null; then
break
fi
if [[ "${attempt}" == "45" ]]; then
docker compose -p "${compose_project}" logs app
exit 1
fi
sleep 2
done
auth_header="Authorization: Bearer ${core_api_key}"
health_body="$(curl -fsS "http://localhost:${app_port}/health")"
test "$(jq -r .status <<<"${health_body}")" = "ok"
memories_health="$(curl -fsS -H "${auth_header}" "http://localhost:${app_port}/v1/memories/health")"
test "$(jq -r .status <<<"${memories_health}")" = "ok"
stats_status="$(curl -fsS -o /dev/null -w '%{http_code}' \
-H "${auth_header}" \
-G "http://localhost:${app_port}/v1/memories/stats" \
--data-urlencode "user_id=smoke-test-user")"
test "${stats_status}" = "200"
ingest_response="$(curl -fsS -w '\n%{http_code}' \
-X POST "http://localhost:${app_port}/v1/memories/ingest/quick" \
-H "${auth_header}" \
-H "Content-Type: application/json" \
-d '{
"user_id": "smoke-test-user",
"conversation": "User: I am testing the Docker deployment. The project uses PostgreSQL and Next.js.",
"source_site": "docker-smoke-test"
}')"
ingest_status="$(tail -n 1 <<<"${ingest_response}")"
ingest_body="$(sed '$d' <<<"${ingest_response}")"
test "${ingest_status}" = "200"
test "$(jq -r '.memories_stored // .memoriesStored // 0' <<<"${ingest_body}")" -ge 1
search_response="$(curl -fsS -w '\n%{http_code}' \
-X POST "http://localhost:${app_port}/v1/memories/search" \
-H "${auth_header}" \
-H "Content-Type: application/json" \
-d '{
"user_id": "smoke-test-user",
"query": "What database is the project using?",
"source_site": "docker-smoke-test"
}')"
search_status="$(tail -n 1 <<<"${search_response}")"
search_body="$(sed '$d' <<<"${search_response}")"
test "${search_status}" = "200"
test "$(jq -r .count <<<"${search_body}")" -ge 1
bad_status="$(curl -sS -o /dev/null -w '%{http_code}' \
-X POST "http://localhost:${app_port}/v1/memories/ingest" \
-H "${auth_header}" \
-H "Content-Type: application/json" \
-d '{"user_id":"x"}')"
test "${bad_status}" = "400"
- name: Build and push multi-platform release tags
if: steps.package.outputs.should_publish == 'true' && steps.package.outputs.retag_latest_only != 'true'
run: |
set -euo pipefail
tag_args=(
--tag "${IMAGE_NAME}:${{ steps.package.outputs.version }}"
--tag "${IMAGE_NAME}:${{ steps.package.outputs.short_sha }}"
--tag "${IMAGE_NAME}:sha-${{ steps.package.outputs.short_sha }}"
)
if [[ "${{ steps.package.outputs.is_latest }}" == "true" ]]; then
tag_args+=(--tag "${IMAGE_NAME}:latest")
else
echo "Not moving latest: ${{ steps.package.outputs.version }} is not npm latest."
fi
docker buildx build \
--platform linux/amd64,linux/arm64 \
--file release-source/packages/core/Dockerfile \
--label "org.opencontainers.image.source=https://github.com/${GITHUB_REPOSITORY}" \
--label "org.opencontainers.image.revision=${{ steps.package.outputs.git_head }}" \
--label "org.opencontainers.image.version=${{ steps.package.outputs.version }}" \
--label "org.opencontainers.image.title=@atomicmemory/core" \
"${tag_args[@]}" \
--push \
release-source
- name: Move latest to existing version image
if: steps.package.outputs.should_publish == 'true' && steps.package.outputs.retag_latest_only == 'true'
run: |
set -euo pipefail
docker buildx imagetools create \
--tag "${IMAGE_NAME}:latest" \
"${IMAGE_NAME}:${{ steps.package.outputs.version }}"
- name: Verify release platforms
if: steps.package.outputs.should_publish == 'true'
run: |
set -euo pipefail
assert_platform() {
local image_ref="$1"
local os="$2"
local arch="$3"
docker manifest inspect "${image_ref}" --verbose | jq -e --arg os "${os}" --arg arch "${arch}" '
if type == "array" then
any(.[]; .Descriptor.platform.os == $os and .Descriptor.platform.architecture == $arch)
else
.Descriptor.platform.os == $os and .Descriptor.platform.architecture == $arch
end
' >/dev/null
}
verify_tag() {
local image_ref="$1"
for attempt in {1..12}; do
if assert_platform "${image_ref}" linux amd64 && assert_platform "${image_ref}" linux arm64; then
echo "${image_ref} includes linux/amd64 and linux/arm64."
return 0
fi
sleep 5
done
echo "::error::${image_ref} does not include both linux/amd64 and linux/arm64."
exit 1
}
verify_tag "${IMAGE_NAME}:${{ steps.package.outputs.version }}"
verify_tag "${IMAGE_NAME}:${{ steps.package.outputs.short_sha }}"
verify_tag "${IMAGE_NAME}:sha-${{ steps.package.outputs.short_sha }}"
if [[ "${{ steps.package.outputs.is_latest }}" == "true" ]]; then
verify_tag "${IMAGE_NAME}:latest"
fi