feat(dashboard): add litkey payment entrypoint (#395) #251
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 }} |