diff --git a/.github/workflows/ccip-server-docker.yml b/.github/workflows/ccip-server-docker.yml new file mode 100644 index 00000000000..c324ad85190 --- /dev/null +++ b/.github/workflows/ccip-server-docker.yml @@ -0,0 +1,124 @@ +name: Build and Push CCIP Server Image to GCR +on: + push: + branches: [main] + tags: + - '**' + pull_request: + paths: + - 'typescript/ccip-server/**' + - '.github/workflows/ccip-server-docker.yml' + workflow_dispatch: + inputs: + include_arm64: + description: 'Include arm64 in the build' + required: false + default: 'false' + +concurrency: + group: build-push-ccip-server-${{ github.ref }} + cancel-in-progress: true + +jobs: + check-env: + runs-on: ubuntu-latest + outputs: + gcloud-service-key: ${{ steps.gcloud-service-key.outputs.defined }} + steps: + - id: gcloud-service-key + env: + GCLOUD_SERVICE_KEY: ${{ secrets.GCLOUD_SERVICE_KEY }} + if: "${{ env.GCLOUD_SERVICE_KEY != '' }}" + run: echo "defined=true" >> $GITHUB_OUTPUT + + build-and-push-to-gcr: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + pull-requests: write + + needs: [check-env] + if: needs.check-env.outputs.gcloud-service-key == 'true' + + steps: + - name: Generate GitHub App Token + id: generate-token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ secrets.HYPER_GONK_APP_ID }} + private-key: ${{ secrets.HYPER_GONK_PRIVATE_KEY }} + + - uses: actions/checkout@v5 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + submodules: recursive + fetch-depth: 0 + + - name: Generate tag data + id: taggen + run: | + echo "TAG_DATE=$(date +'%Y%m%d-%H%M%S')" >> $GITHUB_OUTPUT + echo "TAG_SHA=$(echo '${{ github.event.pull_request.head.sha || github.sha }}' | cut -b 1-7)" >> $GITHUB_OUTPUT + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: | + gcr.io/abacus-labs-dev/hyperlane-offchain-lookup-server + tags: | + type=ref,event=branch + type=ref,event=pr + type=raw,value=${{ steps.taggen.outputs.TAG_SHA }}-${{ steps.taggen.outputs.TAG_DATE }} + + - name: Set up Depot CLI + uses: depot/setup-action@v1 + + - name: Login to GCR + uses: docker/login-action@v3 + with: + registry: gcr.io + username: _json_key + password: ${{ secrets.GCLOUD_SERVICE_KEY }} + + - name: Determine platforms + id: determine-platforms + run: | + if [ "${{ github.event.inputs.include_arm64 }}" == "true" ]; then + echo "platforms=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT + else + echo "platforms=linux/amd64" >> $GITHUB_OUTPUT + fi + + - name: Get Foundry version + id: foundry-version + run: | + FOUNDRY_VERSION=$(cat solidity/.foundryrc) + echo "FOUNDRY_VERSION=$FOUNDRY_VERSION" >> $GITHUB_OUTPUT + + - name: Build and push + id: build + uses: depot/build-push-action@v1 + with: + project: 3cpjhx94qv + context: ./ + file: ./typescript/ccip-server/Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: ${{ steps.determine-platforms.outputs.platforms }} + build-args: | + FOUNDRY_VERSION=${{ steps.foundry-version.outputs.FOUNDRY_VERSION }} + + - name: Comment image tags on PR + if: github.event_name == 'pull_request' + uses: ./.github/actions/docker-image-comment + with: + comment_tag: ccip-server-docker-image + image_name: CCIP Server Docker Image + emoji: 🔍 + image_tags: ${{ steps.meta.outputs.tags }} + pr_number: ${{ github.event.pull_request.number }} + github_token: ${{ steps.generate-token.outputs.token }} + job_status: ${{ job.status }} diff --git a/Dockerfile b/Dockerfile index d80e6ac3a9b..ddebd938a4c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -42,6 +42,9 @@ COPY solidity/package.json ./solidity/ COPY solhint-plugin/package.json ./solhint-plugin/ COPY starknet/package.json ./starknet/ +# Set dummy DATABASE_URL for ccip-server prisma generate during install +ENV DATABASE_URL="postgresql://placeholder:placeholder@localhost:5432/placeholder" + RUN pnpm install --frozen-lockfile && pnpm store prune # Copy everything else diff --git a/turbo.json b/turbo.json index dd55c4e1d94..5b5a79cbdda 100644 --- a/turbo.json +++ b/turbo.json @@ -26,6 +26,10 @@ "coverage": { "dependsOn": ["build"], "outputs": ["coverage/**", "fixtures/**"] + }, + "bundle": { + "dependsOn": ["^build"], + "outputs": ["*-bundle/**"] } } } diff --git a/typescript/ccip-server/Dockerfile b/typescript/ccip-server/Dockerfile new file mode 100644 index 00000000000..c903a7d3477 --- /dev/null +++ b/typescript/ccip-server/Dockerfile @@ -0,0 +1,105 @@ +FROM node:20-slim AS builder + +WORKDIR /hyperlane-monorepo + +RUN apt-get update && apt-get install -y --no-install-recommends \ + git g++ make python3 python3-pip jq bash curl ca-certificates unzip \ + && rm -rf /var/lib/apt/lists/* + +# Install Foundry (Linux binaries) - pinned version for reproducibility +ARG FOUNDRY_VERSION +ARG TARGETARCH +SHELL ["/bin/bash", "-c"] +RUN set -o pipefail && \ + ARCH=$([ "$TARGETARCH" = "arm64" ] && echo "arm64" || echo "amd64") && \ + curl --fail -L "https://github.com/foundry-rs/foundry/releases/download/${FOUNDRY_VERSION}/foundry_${FOUNDRY_VERSION}_linux_${ARCH}.tar.gz" | tar -xzC /usr/local/bin forge cast +SHELL ["/bin/sh", "-c"] + +# Copy package.json first for corepack to read packageManager field +COPY package.json ./ +RUN corepack enable && corepack install + +# Copy pnpm config files +COPY pnpm-lock.yaml pnpm-workspace.yaml ./ + +# Copy patches directory (required for pnpm install) +COPY patches ./patches + +# Copy only the packages needed for ccip-server +COPY typescript/ccip-server/package.json ./typescript/ccip-server/ +COPY typescript/deploy-sdk/package.json ./typescript/deploy-sdk/ +COPY typescript/sdk/package.json ./typescript/sdk/ +COPY typescript/provider-sdk/package.json ./typescript/provider-sdk/ +COPY typescript/utils/package.json ./typescript/utils/ +COPY typescript/cosmos-sdk/package.json ./typescript/cosmos-sdk/ +COPY typescript/cosmos-types/package.json ./typescript/cosmos-types/ +COPY typescript/radix-sdk/package.json ./typescript/radix-sdk/ +COPY typescript/tsconfig/package.json ./typescript/tsconfig/ +COPY typescript/eslint-config/package.json ./typescript/eslint-config/ +COPY solidity/package.json ./solidity/ +COPY solhint-plugin/package.json ./solhint-plugin/ +COPY starknet/package.json ./starknet/ + +# Copy prisma schema before install (needed for postinstall prisma generate) +COPY typescript/ccip-server/prisma ./typescript/ccip-server/prisma + +# Set dummy DATABASE_URL for prisma generate during install (actual URL provided at runtime) +ENV DATABASE_URL="postgresql://placeholder:placeholder@localhost:5432/placeholder" + +RUN pnpm install --frozen-lockfile + +# Run prisma generate after install (needed to generate Prisma client) +RUN pnpm --filter @hyperlane-xyz/ccip-server prisma generate + +# Copy source files +COPY turbo.json ./ +COPY typescript/ccip-server ./typescript/ccip-server +COPY typescript/deploy-sdk ./typescript/deploy-sdk +COPY typescript/sdk ./typescript/sdk +COPY typescript/provider-sdk ./typescript/provider-sdk +COPY typescript/utils ./typescript/utils +COPY typescript/cosmos-sdk ./typescript/cosmos-sdk +COPY typescript/cosmos-types ./typescript/cosmos-types +COPY typescript/radix-sdk ./typescript/radix-sdk +COPY typescript/tsconfig ./typescript/tsconfig +COPY typescript/eslint-config ./typescript/eslint-config +COPY solidity ./solidity +COPY solhint-plugin ./solhint-plugin +COPY starknet ./starknet + +# Build the ccip-server +RUN pnpm turbo run build --filter=@hyperlane-xyz/ccip-server + +# Create standalone deployment with resolved dependencies (no symlinks) +# --legacy flag required for pnpm v10+ without inject-workspace-packages +RUN pnpm --filter @hyperlane-xyz/ccip-server deploy --legacy --prod /app + +# Copy generated Prisma client to dist (TypeScript doesn't copy non-TS files) +# Note: Prisma outputs to src/generated/prisma/ (custom path), not node_modules/.prisma +RUN cp -r /app/src/generated /app/dist/generated + +# Production stage - Debian slim for Prisma native binary compatibility +FROM node:20-slim AS runner + +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Copy the deployed standalone package +COPY --from=builder /app ./ + +# Copy prisma schema for migrations +COPY --from=builder /hyperlane-monorepo/typescript/ccip-server/prisma ./prisma + +# Environment variables +ENV NODE_ENV=production +ENV LOG_LEVEL=info +ENV SERVER_PORT=3000 + +# Expose ports +EXPOSE 3000 +EXPOSE 9090 + +# Run the ccip-server +CMD ["node", "dist/server.js"] diff --git a/typescript/ccip-server/package.json b/typescript/ccip-server/package.json index fc928819c4e..6636c38dc84 100644 --- a/typescript/ccip-server/package.json +++ b/typescript/ccip-server/package.json @@ -6,7 +6,8 @@ "typedocMain": "src/index.ts", "private": true, "files": [ - "src" + "src", + "dist" ], "engines": { "node": ">=16" diff --git a/typescript/ccip-server/prisma/schema.prisma b/typescript/ccip-server/prisma/schema.prisma index 55d8d8f8555..5f1da28a49e 100644 --- a/typescript/ccip-server/prisma/schema.prisma +++ b/typescript/ccip-server/prisma/schema.prisma @@ -6,6 +6,7 @@ generator client { output = "../src/generated/prisma" } +// DATABASE_URL is required for prisma generate but actual connection is handled in code datasource db { provider = "postgresql" url = env("DATABASE_URL") diff --git a/typescript/infra/helm/offchain-lookup-server/templates/deployment.yaml b/typescript/infra/helm/offchain-lookup-server/templates/deployment.yaml index d382de40def..5acb4575452 100644 --- a/typescript/infra/helm/offchain-lookup-server/templates/deployment.yaml +++ b/typescript/infra/helm/offchain-lookup-server/templates/deployment.yaml @@ -1,7 +1,6 @@ # Container Deployment -# The offchain-lookup typescript server, started with pnpm. -# It is used for the ccip-read ISM -# And can expose multiple endpoints depending on its ENV. +# The offchain-lookup typescript server (CCIP-read ISM) +# Can expose multiple endpoints depending on its ENV. # The server code can be found in /typescript/ccip-server apiVersion: apps/v1 kind: Deployment @@ -32,8 +31,6 @@ spec: initialDelaySeconds: 5 periodSeconds: 10 imagePullPolicy: IfNotPresent - command: ["pnpm"] - args: ["-C", "typescript/ccip-server", "run", "start"] ports: - name: http containerPort: {{ .Values.port }} diff --git a/typescript/infra/helm/offchain-lookup-server/values-mainnet.yaml b/typescript/infra/helm/offchain-lookup-server/values-mainnet.yaml index 564c9f08117..36a766c7380 100644 --- a/typescript/infra/helm/offchain-lookup-server/values-mainnet.yaml +++ b/typescript/infra/helm/offchain-lookup-server/values-mainnet.yaml @@ -2,11 +2,11 @@ environment: mainnet # General deployment configuration image: - repository: gcr.io/abacus-labs-dev/hyperlane-monorepo + repository: gcr.io/abacus-labs-dev/hyperlane-offchain-lookup-server # Modify this tag to deploy a new revision. # Images can be found here: - # https://console.cloud.google.com/artifacts/docker/abacus-labs-dev/us/gcr.io/hyperlane-monorepo?inv=1&invt=AbxRMg&project=abacus-labs-dev - tag: 8da6852-20251215-172511 + # https://console.cloud.google.com/artifacts/docker/abacus-labs-dev/us/gcr.io/hyperlane-offchain-lookup-server + tag: 8a41886-20251222-170941 # In Google Cloud Secret Manager, all secrets need to have a certain prefix in order to be accessible by # the Cluster Secret Store. For testnet this prefix is "hyperlane-testnet4" diff --git a/typescript/infra/helm/offchain-lookup-server/values-testnet.yaml b/typescript/infra/helm/offchain-lookup-server/values-testnet.yaml index 330f938d087..22fc7d84e94 100644 --- a/typescript/infra/helm/offchain-lookup-server/values-testnet.yaml +++ b/typescript/infra/helm/offchain-lookup-server/values-testnet.yaml @@ -2,11 +2,11 @@ environment: testnet # General deployment configuration image: - repository: gcr.io/abacus-labs-dev/hyperlane-monorepo + repository: gcr.io/abacus-labs-dev/hyperlane-offchain-lookup-server # Modify this tag to deploy a new revision. # Images can be found here: - # https://console.cloud.google.com/artifacts/docker/abacus-labs-dev/us/gcr.io/hyperlane-monorepo?inv=1&invt=AbxRMg&project=abacus-labs-dev - tag: 8da6852-20251215-172511 + # https://console.cloud.google.com/artifacts/docker/abacus-labs-dev/us/gcr.io/hyperlane-offchain-lookup-server + tag: 8a41886-20251222-170941 # In Google Cloud Secret Manager, all secrets need to have a certain prefix in order to be accessible by # the Cluster Secret Store. For testnet this prefix is "hyperlane-testnet4" diff --git a/typescript/infra/helm/offchain-lookup-server/values.yaml b/typescript/infra/helm/offchain-lookup-server/values.yaml index 5404ce85aa9..cec6b0f7c20 100644 --- a/typescript/infra/helm/offchain-lookup-server/values.yaml +++ b/typescript/infra/helm/offchain-lookup-server/values.yaml @@ -2,11 +2,11 @@ environment: testnet # General deployment configuration image: - repository: gcr.io/abacus-labs-dev/hyperlane-monorepo + repository: gcr.io/abacus-labs-dev/hyperlane-offchain-lookup-server # Modify this tag to deploy a new revision. # Images can be found here: - # https://console.cloud.google.com/artifacts/docker/abacus-labs-dev/us/gcr.io/hyperlane-monorepo?inv=1&invt=AbxRMg&project=abacus-labs-dev - tag: 8da6852-20251215-172511 + # https://console.cloud.google.com/artifacts/docker/abacus-labs-dev/us/gcr.io/hyperlane-offchain-lookup-server + tag: 8a41886-20251222-170941 secrets: name: 'offchain-lookup-server'