Skip to content
Draft
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
110 changes: 110 additions & 0 deletions .github/workflows/build-push-ecr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
name: Build and Push to AWS ECR

on:
pull_request:
types: [ closed ]
branches: [ main ]
paths:
- 'src/**'
- 'contracts/**'
- 'Dockerfile'
- 'package.json'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
workflow_dispatch:

env:
AWS_REGION: us-west-2
ECR_REPOSITORY: offchain-labs/ultra-relay

permissions:
contents: write
id-token: write

jobs:
build-and-push:
if: github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch'
runs-on: ocl-2cpu-8ram-dind

steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
submodules: recursive
persist-credentials: false

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b
with:
role-to-assume: ${{ vars.AWS_ROLE_ARN }}
role-session-name: GitHubActions-${{ github.run_id }}
aws-region: ${{ env.AWS_REGION }}

- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@d539f0932e70871a027e9d5a9d8fc38589180a64

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5

- name: Extract metadata
id: meta
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9
with:
images: ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}
tags: |
type=ref,event=branch
type=sha,prefix={{branch}}-
type=raw,value=latest,enable={{is_default_branch}}

- name: Build and push Docker image
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf
with:
context: .
file: ./Dockerfile
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:buildcache
cache-to: type=registry,ref=${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:buildcache,mode=max,image-manifest=true,oci-mediatypes=true

- name: Generate release notes
id: release-notes
env:
REGISTRY: ${{ steps.login-ecr.outputs.registry }}
run: |
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")

RELEASE_NOTES=$(git log ${LATEST_TAG}..HEAD --pretty=format:"- %s (%h)" --no-merges | head -20)

if [ -z "$RELEASE_NOTES" ]; then
RELEASE_NOTES="- Initial release"
fi

echo "notes<<EOF" >> $GITHUB_OUTPUT
echo "## Changes" >> $GITHUB_OUTPUT
echo "$RELEASE_NOTES" >> $GITHUB_OUTPUT
echo "" >> $GITHUB_OUTPUT
echo "## Docker Images" >> $GITHUB_OUTPUT
echo "- \`${REGISTRY}/${ECR_REPOSITORY}:latest\`" >> $GITHUB_OUTPUT
echo "- \`${REGISTRY}/${ECR_REPOSITORY}:main-${GITHUB_SHA}\`" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT

- name: Create GitHub release
if: github.event_name != 'workflow_dispatch'
uses: actions/github-script@d746ffe35508b1917358783b479e04febd2b8f71 # v9.0.0
env:
RELEASE_TAG: v${{ github.run_number }}
RELEASE_NOTES: ${{ steps.release-notes.outputs.notes }}
with:
script: |
await github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: process.env.RELEASE_TAG,
name: `Release ${process.env.RELEASE_TAG}`,
body: process.env.RELEASE_NOTES,
draft: false,
prerelease: false,
});
103 changes: 103 additions & 0 deletions .github/workflows/verify-staging-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
name: Verify staging image exists in ECR

# Guardrail for "Release staging" PRs.
#
# The deploy tag in deploy/staging.values.yaml must point at an image that
# actually exists in ECR. build-push-ecr.yml only builds an image when a
# merged PR touches a build-relevant path (src/**, contracts/**, Dockerfile,
# package.json, pnpm-lock.yaml, pnpm-workspace.yaml). A tag whose source PR
# changed only other paths (test/**, docs/**, README) is never built, so
# pointing staging at it makes the rollout fail with ImagePullBackOff (image
# "not found"). This check fails such a PR before it can merge and reach the
# cluster.
#
# Make this a *required* status check in branch protection so it actually
# blocks merge. It runs on every PR but no-ops (success) unless staging
# values changed, so it is safe to require -- a paths: filter would instead
# leave unrelated PRs hanging on a required check that never reports.
#
# To extend the gate to other environments, add their values files to the
# git diff in "Detect changed staging values" (e.g. deploy/prod.values.yaml).

on:
pull_request:
branches: [ main ]

permissions:
contents: read
id-token: write

env:
AWS_REGION: us-west-2
ECR_REPOSITORY: offchain-labs/ultra-relay

jobs:
verify-image-tag:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
fetch-depth: 0
persist-credentials: false

- name: Detect changed staging values
id: changed
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
files=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" -- \
deploy/staging.values.yaml)
if [ -z "$files" ]; then
echo "changed=" >> "$GITHUB_OUTPUT"
echo "No staging values changed by this PR; image-tag check is a no-op."
else
echo "changed=$(echo "$files" | tr '\n' ' ')" >> "$GITHUB_OUTPUT"
echo "Changed staging values:"
echo "$files"
fi

- name: Configure AWS credentials
if: steps.changed.outputs.changed != ''
uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b
with:
role-to-assume: ${{ vars.AWS_ROLE_ARN }}
role-session-name: GitHubActions-verify-image-${{ github.run_id }}
aws-region: ${{ env.AWS_REGION }}

- name: Verify referenced image tags exist in ECR
if: steps.changed.outputs.changed != ''
env:
CHANGED_FILES: ${{ steps.changed.outputs.changed }}
run: |
set -euo pipefail

mapfile -t tags < <(
for f in $CHANGED_FILES; do
yq '.. | select(tag == "!!map") | select(has("repository") and has("tag")) | select(.repository | test("'"$ECR_REPOSITORY"'$")) | .tag' "$f"
done | grep -Ev '^(null|)$' | sort -u
)

if [ "${#tags[@]}" -eq 0 ]; then
echo "Changed staging values reference no ${ECR_REPOSITORY} image tag; nothing to verify."
exit 0
fi

status=0
for tag in "${tags[@]}"; do
if err=$(aws ecr describe-images \
--repository-name "$ECR_REPOSITORY" \
--image-ids imageTag="$tag" \
--region "$AWS_REGION" 2>&1 >/dev/null); then
echo "ok: $ECR_REPOSITORY:$tag exists in ECR"
elif printf '%s' "$err" | grep -q 'ImageNotFoundException'; then
echo "::error::$ECR_REPOSITORY:$tag is NOT in ECR. build-push-ecr only builds images for PRs touching src/**, contracts/**, Dockerfile, package.json, pnpm-lock.yaml or pnpm-workspace.yaml -- a tag whose source PR changed only other paths (e.g. test/**, docs/**) is never built. Re-point the tag to a commit that has a confirmed image (an existing ECR tag or a published Release)."
status=1
else
echo "::error::Failed to query ECR for $ECR_REPOSITORY:$tag: $err"
status=1
fi
done
exit $status
121 changes: 121 additions & 0 deletions deploy/common.values.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
application:
applicationName: ultra-relay

labels:
app: ultra-relay
component: ultra-relay

serviceMonitor:
enabled: true
endpoints:
- interval: 15s
path: /metrics
port: http

rbac:
enabled: true
serviceAccount:
enabled: true
name: ultra-relay

deployment:
enabled: true
replicas: 2
reloadOnChange: true
configMapChecksum: true
serviceAccountName: ultra-relay
# image.repository (ECR registry, references the OCL AWS account) is set from
# zerodev-helm-charts' private $secrets values, not committed in this public repo.
# Dockerfile never sets a non-root USER, so the chart's default runAsNonRoot/readOnlyRootFilesystem would fail admission.
containerSecurityContext:
readOnlyRootFilesystem: false
runAsNonRoot: false
# No CLI flag reads these from a file path; the wrapper reads the mounted secret files into flag values itself.
command:
- /bin/sh
- -c
args:
- >-
exec pnpm start run
--config /configs/alto-config.json
--executor-private-keys "$(cat /secrets/executor-private-keys)"
--utility-private-key "$(cat /secrets/utility-private-key)"
env:
SENTRY_DSN:
valueFrom:
secretKeyRef:
name: ultra-relay-secrets
key: SENTRY_DSN
BETTER_STACK_TOKEN:
valueFrom:
secretKeyRef:
name: ultra-relay-secrets
key: BETTER_STACK_TOKEN
BETTER_STACK_ENDPOINT:
valueFrom:
secretKeyRef:
name: ultra-relay-secrets
key: BETTER_STACK_ENDPOINT
ports:
- containerPort: 3000
name: http
protocol: TCP
# Long grace period so in-flight mempool restoration (CONTEXT.md) finishes before SIGKILL.
terminationGracePeriodSeconds: 60
livenessProbe:
enabled: true
httpGet:
path: /health
port: http
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
enabled: true
httpGet:
path: /health
port: http
initialDelaySeconds: 5
periodSeconds: 10
volumeMounts:
config:
mountPath: /configs/alto-config.json
subPath: alto-config.json
secrets:
mountPath: /secrets
readOnly: true
volumes:
config:
configMap:
name: ultra-relay-config
secrets:
secret:
secretName: ultra-relay-secrets
items:
- key: EXECUTOR_PRIVATE_KEYS
path: executor-private-keys
- key: UTILITY_PRIVATE_KEY
path: utility-private-key
resources:
limits:
memory: 512Mi
requests:
cpu: 250m
memory: 256Mi
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app.kubernetes.io/name: ultra-relay

service:
enabled: true
ports:
- port: 3000
targetPort: 3000
protocol: TCP
name: http

configMap:
enabled: true
47 changes: 47 additions & 0 deletions deploy/prod.values.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
application:
deployment:
image:
tag: CHANGEME

resources:
limits:
memory: 1Gi
requests:
cpu: 500m
memory: 512Mi

configMap:
files:
config:
# TODO(zerodev/sre): placeholder chain config, not live values.
alto-config.json: |
{
"network-name": "CHANGEME-prod",
"log-environment": "production",
"entrypoints": "CHANGEME",
"rpc-url": "CHANGEME",
"port": 3000,
"safe-mode": true,
"enable-horizontal-scaling": true,
"enable-redis-receipt-cache": true,
"redis-endpoint": "CHANGEME-prod-ultra-relay-elasticache-endpoint",
"redis-key-prefix": "alto-prod"
}

ingress:
enabled: true
ingressClassName: "alb"
annotations:
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS":443}]'
alb.ingress.kubernetes.io/ssl-redirect: '443'
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: "ip"
alb.ingress.kubernetes.io/healthcheck-port: '3000'
alb.ingress.kubernetes.io/healthcheck-path: '/health'
alb.ingress.kubernetes.io/backend-protocol: HTTP
hosts:
- host: aa.zerodev.app # CHANGEME - confirm with team
paths:
- path: /
pathType: Prefix
servicePort: http
Loading