Skip to content

feat(dashboard): add litkey payment entrypoint (#395) #251

feat(dashboard): add litkey payment entrypoint (#395)

feat(dashboard): add litkey payment entrypoint (#395) #251

# Deploy lit-api-server + lit-actions + otel-collector to Phala CVM
#
# Deployment targets:
# push to next → chipotle-next (direct automated deploy)
# push to main → chipotle-dev (direct automated deploy)
#
# For production (v* tag) deploys, see deploy-prod-1-propose.yml and deploy-prod-2-execute.yml.
#
# Image provenance (DR-1b): Each image is signed with Sigstore (cosign keyless) immediately
# after push. Signatures are recorded in the public Rekor transparency log, binding the image
# digest to the GitHub Actions OIDC identity that built it. This allows anyone to verify that
# a given image digest was built from a specific commit in this repository.
#
# To verify an image:
# cosign verify \
# --certificate-identity-regexp "https://github.com/LIT-Protocol/chipotle/.*" \
# --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
# <image>@<digest>
#
# Required GitHub secrets:
# DOCKERHUB_TOKEN - Docker Hub PAT (Account Settings > Security > Access Tokens)
# PHALA_CLOUD_API_KEY - From Phala Cloud Dashboard > API Tokens
# PHALA_DSTACKAPP_PRIVATE_KEY - Private key for DstackApp on-chain KMS
# STRIPE_SANDBOX_SECRET_KEY - Stripe sandbox/test secret key
# STRIPE_SANDBOX_PUBLISHABLE_KEY - Stripe sandbox/test publishable key
# GCP_SERVICE_ACCOUNT_JSON - GCP service account key (raw JSON or base64-encoded)
# BASE_CHAIN_RPC - RPC endpoint for Base blockchain
# CERTBOT_AWS_ACCESS_KEY_ID - Route 53 IAM access key for DNS-01 challenge
# CERTBOT_AWS_SECRET_ACCESS_KEY - Route 53 IAM secret key for DNS-01 challenge
# CERTBOT_AWS_ROLE_ARN - IAM role ARN for STS assumption (required by dstack-ingress)
#
# Required GitHub vars:
# DOCKER_IMAGE - Base image path, e.g. docker.io/username/chipotle
# Three images are pushed:
# ${DOCKER_IMAGE}-lit-actions
# ${DOCKER_IMAGE}-lit-api-server
# ${DOCKER_IMAGE}-otel-collector
# DOCKERHUB_USERNAME - Docker Hub username
#
# Required Phala CVM encrypted env vars (per instance, set via Phala Cloud Dashboard):
# BASE_CHAIN_RPC - RPC endpoint for the chain
# GCP_SERVICE_ACCOUNT_JSON - GCP service account key (raw JSON or base64-encoded)
# GCP_PROJECT_ID - GCP project ID (e.g. "my-gcp-project")
# STRIPE_SECRET_KEY - Stripe API secret key (test sandbox for staging, live for prod)
# STRIPE_PUBLISHABLE_KEY - Stripe API publishable key (test sandbox for staging, live for prod)
# CERTBOT_AWS_ACCESS_KEY_ID - Route 53 IAM access key for DNS-01 challenge
# CERTBOT_AWS_ROLE_ARN - IAM role ARN for STS assumption (required by dstack-ingress)
# CERTBOT_AWS_REGION - AWS region for STS endpoint (e.g. "us-east-1")
name: "Deploy Staging"
# Only one deploy job per-branch: abort any running job on same branch when a new commit is pushed.
concurrency:
group: deploy-phala-${{ github.workflow }}-${{ github.head_ref || github.ref_name }}
cancel-in-progress: true
on:
push:
branches: [main, next]
workflow_dispatch:
jobs:
validate-secrets:
runs-on: self-hosted
steps:
- name: Validate required secrets
env:
GCP_SERVICE_ACCOUNT_JSON: ${{ secrets.GCP_SERVICE_ACCOUNT_JSON }}
BASE_CHAIN_RPC: ${{ secrets.BASE_CHAIN_RPC }}
CERTBOT_AWS_ACCESS_KEY_ID: ${{ secrets.CERTBOT_AWS_ACCESS_KEY_ID }}
CERTBOT_AWS_SECRET_ACCESS_KEY: ${{ secrets.CERTBOT_AWS_SECRET_ACCESS_KEY }}
CERTBOT_AWS_ROLE_ARN: ${{ secrets.CERTBOT_AWS_ROLE_ARN }}
PHALA_CLOUD_API_KEY: ${{ secrets.PHALA_CLOUD_API_KEY }}
PHALA_DSTACKAPP_PRIVATE_KEY: ${{ secrets.PHALA_DSTACKAPP_PRIVATE_KEY }}
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SANDBOX_SECRET_KEY }}
STRIPE_PUBLISHABLE_KEY: ${{ secrets.STRIPE_SANDBOX_PUBLISHABLE_KEY }}
run: |
missing=()
for secret in GCP_SERVICE_ACCOUNT_JSON BASE_CHAIN_RPC CERTBOT_AWS_ACCESS_KEY_ID CERTBOT_AWS_SECRET_ACCESS_KEY CERTBOT_AWS_ROLE_ARN PHALA_CLOUD_API_KEY PHALA_DSTACKAPP_PRIVATE_KEY STRIPE_SECRET_KEY STRIPE_PUBLISHABLE_KEY; do
if [ -z "${!secret}" ]; then
missing+=("$secret")
fi
done
if [ ${#missing[@]} -gt 0 ]; then
echo "::error::Missing required GitHub secrets: ${missing[*]}"
exit 1
fi
determine-target:
runs-on: self-hosted
outputs:
phala_app_name: ${{ steps.set.outputs.phala_app_name }}
base_url: ${{ steps.set.outputs.base_url }}
api_root_url: ${{ steps.set.outputs.api_root_url }}
instance_type: ${{ steps.set.outputs.instance_type }}
gcp_project_id: ${{ steps.set.outputs.gcp_project_id }}
node_config: ${{ steps.set.outputs.node_config }}
domain: ${{ steps.set.outputs.domain }}
steps:
- name: Set deployment target
id: set
run: |
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
echo "phala_app_name=chipotle-dev" >> "$GITHUB_OUTPUT"
echo "instance_type=tdx.large" >> "$GITHUB_OUTPUT"
echo "gcp_project_id=chipotle-dev" >> "$GITHUB_OUTPUT"
echo "node_config=NodeConfig.main.toml" >> "$GITHUB_OUTPUT"
DOMAIN="test.chipotle.litprotocol.com"
elif [ "${{ github.ref }}" = "refs/heads/next" ]; then
# Targets the prod2 instance provisioned via `phala instances add`
# during the prod6→prod2 migration. Shares the same app_id
# (0x969a8c14...) and derived keys as previous chipotle-next
# instances.
echo "phala_app_name=chipotle-next-rep-sa6xj" >> "$GITHUB_OUTPUT"
echo "instance_type=tdx.small" >> "$GITHUB_OUTPUT"
echo "gcp_project_id=chipotle-next" >> "$GITHUB_OUTPUT"
echo "node_config=NodeConfig.next.toml" >> "$GITHUB_OUTPUT"
DOMAIN="test.chipotle.litprotocol.com"
else
echo "Unsupported branch for deployment"
exit 1
fi
echo "domain=${DOMAIN}" >> "$GITHUB_OUTPUT"
echo "base_url=https://${DOMAIN}" >> "$GITHUB_OUTPUT"
echo "api_root_url=https://${DOMAIN}/core/v1" >> "$GITHUB_OUTPUT"
detect-changes:
needs: [determine-target]
runs-on: self-hosted
outputs:
deploy_needed: ${{ steps.check.outputs.deploy_needed }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check if deploy-relevant paths changed
id: check
run: |
API_ROOT_URL="${{ needs.determine-target.outputs.api_root_url }}"
# For workflow_dispatch (manual trigger), always deploy
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "Manual trigger — deploying unconditionally"
echo "deploy_needed=true" >> "$GITHUB_OUTPUT"
exit 0
fi
# Fetch the currently deployed commit version
COMMIT_VERSION=$(curl -sf "${API_ROOT_URL}/version" 2>/dev/null | jq -r '.commit_version // empty' 2>/dev/null || true)
if [ -z "$COMMIT_VERSION" ]; then
echo "Could not reach /version endpoint — deploying"
echo "deploy_needed=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "Deployed commit_version: $COMMIT_VERSION"
# Parse SHA from git describe --always output:
# bare SHA: "abc1234" → "abc1234"
# describe: "v0.3.14-5-gabc1234" → "abc1234"
if echo "$COMMIT_VERSION" | grep -qE -- '-g[0-9a-f]+$'; then
DEPLOYED_SHA=$(echo "$COMMIT_VERSION" | sed 's/.*-g//')
else
DEPLOYED_SHA="$COMMIT_VERSION"
fi
# Verify the SHA exists in the repo
if ! git rev-parse --verify "$DEPLOYED_SHA^{commit}" >/dev/null 2>&1; then
echo "Deployed SHA '$DEPLOYED_SHA' not found in repo history — deploying"
echo "deploy_needed=true" >> "$GITHUB_OUTPUT"
exit 0
fi
# Check for changes in deploy-relevant paths
DEPLOY_PATHS=(
lit-actions/
lit-api-server/
lit-billing-core/
lit-core/
otel-collector/
Dockerfile.lit-actions
Dockerfile.lit-api-server
Dockerfile.otel-collector
docker-compose.phala.yml
.github/workflows/deploy-staging.yml
)
CHANGED=$(git diff --name-only "$DEPLOYED_SHA" HEAD -- "${DEPLOY_PATHS[@]}")
if [ -n "$CHANGED" ]; then
echo "Deploy-relevant changes detected:"
echo "$CHANGED"
echo "deploy_needed=true" >> "$GITHUB_OUTPUT"
else
echo "No deploy-relevant changes since $DEPLOYED_SHA — skipping build & deploy"
echo "deploy_needed=false" >> "$GITHUB_OUTPUT"
fi
build:
needs: [validate-secrets, determine-target, detect-changes]
if: needs.detect-changes.outputs.deploy_needed == 'true'
runs-on: self-hosted
permissions:
id-token: write # required for Sigstore keyless signing (OIDC token)
contents: read
strategy:
fail-fast: false
matrix:
include:
- service: lit-actions
dockerfile: Dockerfile.lit-actions
- service: lit-api-server
dockerfile: Dockerfile.lit-api-server
- service: otel-collector
dockerfile: Dockerfile.otel-collector
steps:
# Check out by commit SHA, not by tag ref. actions/checkout@v4 does a
# "ref fixup" fetch that overwrites annotated tag refs with raw commit
# pointers (git fetch origin +<sha>:refs/tags/<tag>), which causes
# git describe --always to skip the tag. Checking out by SHA avoids the
# fixup entirely, keeping annotated tags intact for git_version!().
- uses: actions/checkout@v4
with:
ref: ${{ github.sha }}
fetch-depth: 0
- name: Install cosign
uses: sigstore/cosign-installer@v3
- name: Log in to Docker Registry
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push ${{ matrix.service }}
id: push
uses: docker/build-push-action@v6
with:
context: .
file: ./${{ matrix.dockerfile }}
platforms: linux/amd64
push: true
tags: ${{ vars.DOCKER_IMAGE }}-${{ matrix.service }}:${{ github.sha }}
build-args: |
${{ matrix.service == 'lit-api-server' && format('NODE_CONFIG={0}', needs.determine-target.outputs.node_config) || '' }}
- name: Sign image with Sigstore (keyless)
run: |
cosign sign --yes \
"${{ vars.DOCKER_IMAGE }}-${{ matrix.service }}@${{ steps.push.outputs.digest }}"
- name: Save image digest
run: echo "${{ steps.push.outputs.digest }}" > digest-${{ matrix.service }}.txt
- name: Upload digest artifact
uses: actions/upload-artifact@v4
with:
name: digest-${{ matrix.service }}
path: digest-${{ matrix.service }}.txt
deploy:
needs: [determine-target, build, detect-changes]
if: needs.detect-changes.outputs.deploy_needed == 'true'
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
- name: Download image digests
uses: actions/download-artifact@v4
with:
pattern: digest-*
merge-multiple: true
- name: Prepare docker-compose for deployment (digest-pinned)
run: |
DIGEST_LIT_ACTIONS=$(cat digest-lit-actions.txt | tr -d '\n' | sed 's/}[}]*$//')
DIGEST_LIT_API_SERVER=$(cat digest-lit-api-server.txt | tr -d '\n' | sed 's/}[}]*$//')
DIGEST_OTEL_COLLECTOR=$(cat digest-otel-collector.txt | tr -d '\n' | sed 's/}[}]*$//')
DOMAIN="${{ needs.determine-target.outputs.domain }}"
sed \
-e "s|\${DOCKER_IMAGE_LIT_ACTIONS}|${{ vars.DOCKER_IMAGE }}-lit-actions@${DIGEST_LIT_ACTIONS}|g" \
-e "s|\${DOCKER_IMAGE_LIT_API_SERVER}|${{ vars.DOCKER_IMAGE }}-lit-api-server@${DIGEST_LIT_API_SERVER}|g" \
-e "s|\${DOCKER_IMAGE_OTEL_COLLECTOR}|${{ vars.DOCKER_IMAGE }}-otel-collector@${DIGEST_OTEL_COLLECTOR}|g" \
-e "s|\${GCP_PROJECT_ID}|${{ needs.determine-target.outputs.gcp_project_id }}|g" \
-e "s|\${CERTBOT_DOMAIN}|${DOMAIN}|g" \
-e "s|\${CERTBOT_AWS_REGION}|${{ vars.CERTBOT_AWS_REGION }}|g" \
docker-compose.phala.yml > docker-compose.deploy.yml
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install Phala CLI
run: npm install -g phala
- name: Deploy to Phala Cloud
env:
PHALA_CLOUD_API_KEY: ${{ secrets.PHALA_CLOUD_API_KEY }}
PHALA_PRIVATE_KEY: ${{ secrets.PHALA_DSTACKAPP_PRIVATE_KEY }}
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SANDBOX_SECRET_KEY }}
STRIPE_PUBLISHABLE_KEY: ${{ secrets.STRIPE_SANDBOX_PUBLISHABLE_KEY }}
GCP_SERVICE_ACCOUNT_JSON: ${{ secrets.GCP_SERVICE_ACCOUNT_JSON }}
BASE_CHAIN_RPC: ${{ secrets.BASE_CHAIN_RPC }}
CERTBOT_AWS_ACCESS_KEY_ID: ${{ secrets.CERTBOT_AWS_ACCESS_KEY_ID }}
CERTBOT_AWS_SECRET_ACCESS_KEY: ${{ secrets.CERTBOT_AWS_SECRET_ACCESS_KEY }}
CERTBOT_AWS_ROLE_ARN: ${{ secrets.CERTBOT_AWS_ROLE_ARN }}
run: |
phala deploy \
-c docker-compose.deploy.yml \
--cvm-id ${{ needs.determine-target.outputs.phala_app_name }} \
--private-key "$PHALA_PRIVATE_KEY" \
-e "STRIPE_SECRET_KEY=$STRIPE_SECRET_KEY" \
-e "STRIPE_PUBLISHABLE_KEY=$STRIPE_PUBLISHABLE_KEY" \
-e "GCP_SERVICE_ACCOUNT_JSON=$GCP_SERVICE_ACCOUNT_JSON" \
-e "GCP_PROJECT_ID=${{ needs.determine-target.outputs.gcp_project_id }}" \
-e "BASE_CHAIN_RPC=$BASE_CHAIN_RPC" \
-e "CERTBOT_DOMAIN=${{ needs.determine-target.outputs.domain }}" \
-e "CERTBOT_AWS_ACCESS_KEY_ID=$CERTBOT_AWS_ACCESS_KEY_ID" \
-e "CERTBOT_AWS_SECRET_ACCESS_KEY=$CERTBOT_AWS_SECRET_ACCESS_KEY" \
-e "CERTBOT_AWS_ROLE_ARN=$CERTBOT_AWS_ROLE_ARN" \
-e "CERTBOT_AWS_REGION=${{ vars.CERTBOT_AWS_REGION }}"
wait-for-api-available:
needs: [deploy, determine-target, detect-changes]
if: ${{ !cancelled() && needs.determine-target.result == 'success' && needs.detect-changes.result == 'success' && (needs.deploy.result == 'success' || needs.deploy.result == 'skipped') }}
uses: ./.github/workflows/wait-for-api-restart.yml
with:
api_root_url: ${{ needs.determine-target.outputs.api_root_url }}
expected_sha: ${{ needs.detect-changes.outputs.deploy_needed == 'true' && github.sha || '' }}
runner: '["self-hosted"]'
verify-attestation:
needs: [wait-for-api-available, determine-target]
uses: ./.github/workflows/verify-attestation.yml
with:
base_url: ${{ needs.determine-target.outputs.base_url }}
runner: '["self-hosted"]'
openapi-spec-check:
needs: [wait-for-api-available]
uses: ./.github/workflows/openapi-spec-check.yml
with:
api_root_url: ${{ needs.wait-for-api-available.outputs.api_root_url }}
runner: '["self-hosted"]'
k6-smoke:
needs: [wait-for-api-available, openapi-spec-check]
uses: ./.github/workflows/k6-smoke.yml
with:
api_root_url: ${{ needs.wait-for-api-available.outputs.api_root_url }}
k6-correctness:
needs: [wait-for-api-available, k6-smoke]
uses: ./.github/workflows/k6-correctness.yml
with:
api_root_url: ${{ needs.wait-for-api-available.outputs.api_root_url }}