fix: use artifacts to pass TX hashes between matrix jobs #63
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
| # Toy Example App - Build Workflow | |
| # | |
| # Builds and pushes Docker images for both mock-api and enclave. | |
| # Triggered on: | |
| # - Pushes to main and feat/toy-example-app branches | |
| # - Version tags (v*) for automatic deployment to prod5 and prod9 | |
| # | |
| # Images are pushed to GHCR with commit SHA and version tags for traceability. | |
| name: Build Toy Example | |
| on: | |
| push: | |
| branches: [main, feat/toy-example-app] | |
| paths: | |
| - 'toy-example-app/**' | |
| - '.github/workflows/toy-build.yml' | |
| tags: | |
| - 'v[0-9]+.[0-9]+.[0-9]+' | |
| pull_request: | |
| branches: [main, feat/toy-example-app] | |
| paths: | |
| - 'toy-example-app/**' | |
| workflow_dispatch: | |
| inputs: | |
| auto_deploy: | |
| description: 'Automatically deploy to staging after build' | |
| required: false | |
| default: false | |
| type: boolean | |
| env: | |
| REGISTRY: ghcr.io | |
| jobs: | |
| build-mock-api: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| outputs: | |
| image_digest: ${{ steps.build.outputs.digest }} | |
| sha_short: ${{ steps.vars.outputs.sha_short }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Set output variables | |
| id: vars | |
| run: | | |
| echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT | |
| echo "sha_full=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT | |
| echo "timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_OUTPUT | |
| OWNER_LC=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') | |
| echo "image=ghcr.io/${OWNER_LC}/toy-example-mock-api" >> $GITHUB_OUTPUT | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Login to GHCR | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build and push mock-api | |
| id: build | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: ./toy-example-app/mock-api | |
| file: ./toy-example-app/mock-api/Dockerfile | |
| push: ${{ github.event_name != 'pull_request' }} | |
| tags: | | |
| ${{ steps.vars.outputs.image }}:latest | |
| ${{ steps.vars.outputs.image }}:${{ steps.vars.outputs.sha_short }} | |
| labels: | | |
| org.opencontainers.image.source=${{ github.repositoryUrl }} | |
| org.opencontainers.image.revision=${{ steps.vars.outputs.sha_full }} | |
| org.opencontainers.image.created=${{ steps.vars.outputs.timestamp }} | |
| cache-from: type=gha,scope=mock-api | |
| cache-to: type=gha,mode=max,scope=mock-api | |
| - name: Record build info | |
| run: | | |
| echo "## Mock API Build" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "| Item | Value |" >> $GITHUB_STEP_SUMMARY | |
| echo "|------|-------|" >> $GITHUB_STEP_SUMMARY | |
| echo "| Image | \`${{ steps.vars.outputs.image }}:${{ steps.vars.outputs.sha_short }}\` |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Digest | \`${{ steps.build.outputs.digest }}\` |" >> $GITHUB_STEP_SUMMARY | |
| build-enclave: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| outputs: | |
| image_digest: ${{ steps.build.outputs.digest }} | |
| sha_short: ${{ steps.vars.outputs.sha_short }} | |
| version: ${{ steps.vars.outputs.version }} | |
| is_release: ${{ steps.vars.outputs.is_release }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Set output variables | |
| id: vars | |
| run: | | |
| echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT | |
| echo "sha_full=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT | |
| echo "timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_OUTPUT | |
| OWNER_LC=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') | |
| echo "image=ghcr.io/${OWNER_LC}/toy-example-enclave" >> $GITHUB_OUTPUT | |
| # Extract version from tag if present (e.g., v1.2.0 -> 1.2.0) | |
| if [[ "${{ github.ref }}" == refs/tags/v* ]]; then | |
| VERSION="${GITHUB_REF#refs/tags/v}" | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "is_release=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "version=dev" >> $GITHUB_OUTPUT | |
| echo "is_release=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Login to GHCR | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build and push enclave | |
| id: build | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: ./toy-example-app/enclave | |
| file: ./toy-example-app/enclave/Dockerfile | |
| push: ${{ github.event_name != 'pull_request' }} | |
| tags: | | |
| ${{ steps.vars.outputs.image }}:latest | |
| ${{ steps.vars.outputs.image }}:${{ steps.vars.outputs.sha_short }} | |
| ${{ steps.vars.outputs.is_release == 'true' && format('{0}:{1}', steps.vars.outputs.image, steps.vars.outputs.version) || '' }} | |
| build-args: | | |
| BUILD_SHA=${{ steps.vars.outputs.sha_full }} | |
| BUILD_TIME=${{ steps.vars.outputs.timestamp }} | |
| ENVIRONMENT=${{ steps.vars.outputs.is_release == 'true' && 'production' || 'staging' }} | |
| labels: | | |
| org.opencontainers.image.source=${{ github.repositoryUrl }} | |
| org.opencontainers.image.revision=${{ steps.vars.outputs.sha_full }} | |
| org.opencontainers.image.created=${{ steps.vars.outputs.timestamp }} | |
| org.opencontainers.image.version=${{ steps.vars.outputs.version }} | |
| cache-from: type=gha,scope=enclave | |
| cache-to: type=gha,mode=max,scope=enclave | |
| - name: Record build info | |
| run: | | |
| echo "## Enclave Build" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "| Item | Value |" >> $GITHUB_STEP_SUMMARY | |
| echo "|------|-------|" >> $GITHUB_STEP_SUMMARY | |
| echo "| Image | \`${{ steps.vars.outputs.image }}:${{ steps.vars.outputs.sha_short }}\` |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Digest | \`${{ steps.build.outputs.digest }}\` |" >> $GITHUB_STEP_SUMMARY | |
| verify-code: | |
| runs-on: ubuntu-latest | |
| needs: [build-enclave] | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Verify no direct_messages API calls | |
| run: | | |
| echo "## Security Verification" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| # Check for actual API call patterns to direct_messages endpoint | |
| # Looking for URL patterns, not documentation comments | |
| if grep -rE '(/api/direct_messages|getDirectMessages|fetchDirectMessages|\.direct_messages)' toy-example-app/enclave/src/*.ts 2>/dev/null; then | |
| echo "❌ FAIL: Found direct_messages API call patterns in enclave code" >> $GITHUB_STEP_SUMMARY | |
| exit 1 | |
| else | |
| echo "✅ PASS: No direct_messages API calls in enclave code" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| # Also verify no fetch/http calls outside tiktok-client.ts reference direct_messages | |
| NON_CLIENT_FILES=$(find toy-example-app/enclave/src -name "*.ts" ! -name "tiktok-client.ts") | |
| for f in $NON_CLIENT_FILES; do | |
| if grep -E 'direct_message' "$f" | grep -vE '^[[:space:]]*//' | grep -q .; then | |
| echo "❌ FAIL: Non-comment direct_messages reference in $f" >> $GITHUB_STEP_SUMMARY | |
| exit 1 | |
| fi | |
| done | |
| echo "✅ PASS: No non-comment direct_messages references outside tiktok-client.ts" >> $GITHUB_STEP_SUMMARY | |
| - name: Verify API calls are isolated | |
| run: | | |
| # Check that only tiktok-client.ts makes external API calls | |
| FETCH_FILES=$(grep -rl "fetch\(" toy-example-app/enclave/src/ || true) | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### External API Call Analysis" >> $GITHUB_STEP_SUMMARY | |
| if [ -z "$FETCH_FILES" ]; then | |
| echo "No fetch calls found" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "Files with fetch calls:" >> $GITHUB_STEP_SUMMARY | |
| echo "\`\`\`" >> $GITHUB_STEP_SUMMARY | |
| echo "$FETCH_FILES" >> $GITHUB_STEP_SUMMARY | |
| echo "\`\`\`" >> $GITHUB_STEP_SUMMARY | |
| # Verify only tiktok-client.ts | |
| if echo "$FETCH_FILES" | grep -v "tiktok-client.ts"; then | |
| echo "⚠️ WARNING: fetch() found in files other than tiktok-client.ts" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "✅ All external API calls isolated to tiktok-client.ts" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| fi | |
| summary: | |
| runs-on: ubuntu-latest | |
| needs: [build-mock-api, build-enclave, verify-code] | |
| if: always() | |
| outputs: | |
| sha_short: ${{ needs.build-enclave.outputs.sha_short }} | |
| version: ${{ needs.build-enclave.outputs.version }} | |
| is_release: ${{ needs.build-enclave.outputs.is_release }} | |
| all_success: ${{ needs.build-mock-api.result == 'success' && needs.build-enclave.result == 'success' && needs.verify-code.result == 'success' }} | |
| steps: | |
| - name: Create summary | |
| run: | | |
| echo "## Build Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "| Component | Status | SHA |" >> $GITHUB_STEP_SUMMARY | |
| echo "|-----------|--------|-----|" >> $GITHUB_STEP_SUMMARY | |
| echo "| Mock API | ${{ needs.build-mock-api.result }} | ${{ needs.build-mock-api.outputs.sha_short }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Enclave | ${{ needs.build-enclave.result }} | ${{ needs.build-enclave.outputs.sha_short }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Verify | ${{ needs.verify-code.result }} | - |" >> $GITHUB_STEP_SUMMARY | |
| deploy: | |
| runs-on: ubuntu-latest | |
| needs: [summary, build-enclave] | |
| if: | | |
| (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && | |
| needs.summary.outputs.all_success == 'true' | |
| environment: staging | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Set image name | |
| id: vars | |
| run: | | |
| OWNER_LC=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') | |
| echo "enclave_image=ghcr.io/${OWNER_LC}/toy-example-enclave" >> $GITHUB_OUTPUT | |
| - name: Update docker-compose with image tag | |
| run: | | |
| cd toy-example-app/enclave | |
| sed -i "s|image:.*toy-example-enclave.*|image: ${{ steps.vars.outputs.enclave_image }}:${{ needs.build-enclave.outputs.sha_short }}|g" docker-compose.yml | |
| echo "Updated docker-compose.yml:" | |
| cat docker-compose.yml | |
| - name: Calculate compose hash | |
| id: hash | |
| run: | | |
| HASH=$(sha256sum toy-example-app/enclave/docker-compose.yml | cut -d' ' -f1) | |
| echo "compose_hash=$HASH" >> $GITHUB_OUTPUT | |
| echo "Compose hash: $HASH" | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '20' | |
| - name: Install Phala CLI | |
| run: npm install -g phala | |
| - name: Check if enclave files changed | |
| id: changes | |
| run: | | |
| # Check if any files in toy-example-app/enclave changed since the last successful deploy | |
| # We use git diff to compare with the parent commit | |
| # If only workflow or README changed, we can skip deployment | |
| echo "Checking for enclave source changes..." | |
| # Get list of changed files in this commit | |
| CHANGED_FILES=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || echo "") | |
| echo "Changed files in this commit:" | |
| echo "$CHANGED_FILES" | |
| # Check if any enclave source files changed (excluding compose which changes every time) | |
| ENCLAVE_CHANGES=$(echo "$CHANGED_FILES" | grep -E '^toy-example-app/enclave/(src/|Dockerfile|package|tsconfig)' || echo "") | |
| if [ -n "$ENCLAVE_CHANGES" ]; then | |
| echo "Enclave source files changed:" | |
| echo "$ENCLAVE_CHANGES" | |
| echo "enclave_changed=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "No enclave source files changed" | |
| echo "enclave_changed=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Check if deployment needed | |
| id: check | |
| env: | |
| PHALA_CLOUD_API_KEY: ${{ secrets.PHALA_CLOUD_API_KEY }} | |
| run: | | |
| echo "Checking deployment status..." | |
| # Check if CVM exists | |
| CURRENT_INFO=$(phala cvms get toy-example-staging --json 2>/dev/null || echo '{"success":false}') | |
| if echo "$CURRENT_INFO" | jq -e '.success == true' > /dev/null 2>&1; then | |
| CURRENT_STATUS=$(echo "$CURRENT_INFO" | jq -r '.status // "unknown"') | |
| echo "CVM exists with status: $CURRENT_STATUS" | |
| echo "cvm_exists=true" >> $GITHUB_OUTPUT | |
| # If enclave source didn't change and CVM is running, skip deployment | |
| if [ "${{ steps.changes.outputs.enclave_changed }}" = "false" ] && [ "$CURRENT_STATUS" = "running" ]; then | |
| echo "Enclave source unchanged and CVM is running - skipping deployment" | |
| echo "skip_deploy=true" >> $GITHUB_OUTPUT | |
| exit 0 | |
| fi | |
| else | |
| echo "No existing CVM found" | |
| echo "cvm_exists=false" >> $GITHUB_OUTPUT | |
| fi | |
| echo "skip_deploy=false" >> $GITHUB_OUTPUT | |
| - name: Deploy to Phala Cloud | |
| id: deploy | |
| if: steps.check.outputs.skip_deploy != 'true' | |
| env: | |
| PHALA_CLOUD_API_KEY: ${{ secrets.PHALA_CLOUD_API_KEY }} | |
| MOCK_API_URL: ${{ secrets.TOY_MOCK_API_URL }} | |
| MOCK_API_TOKEN: ${{ secrets.TOY_MOCK_API_TOKEN }} | |
| SIGNING_KEY: ${{ secrets.TOY_SIGNING_KEY }} | |
| GHCR_USERNAME: ${{ github.repository_owner }} | |
| GHCR_TOKEN: ${{ secrets.GHCR_PAT }} | |
| PRIVATE_KEY: ${{ secrets.TOY_DEPLOYER_PRIVATE_KEY }} | |
| run: | | |
| cd toy-example-app/enclave | |
| CVM_NAME="toy-example-staging" | |
| # Use --cvm-id to upgrade existing CVM, or create new if doesn't exist | |
| if [ "${{ steps.check.outputs.cvm_exists }}" = "true" ]; then | |
| echo "Upgrading existing CVM..." | |
| DEPLOY_ARGS="--cvm-id $CVM_NAME --wait" | |
| else | |
| echo "Creating new CVM..." | |
| DEPLOY_ARGS="-n $CVM_NAME" | |
| fi | |
| # Deploy/upgrade with Base KMS for on-chain transparency logging | |
| phala deploy \ | |
| -c docker-compose.yml \ | |
| $DEPLOY_ARGS \ | |
| --kms base \ | |
| --private-key "$PRIVATE_KEY" \ | |
| -e MOCK_API_URL="$MOCK_API_URL" \ | |
| -e MOCK_API_TOKEN="$MOCK_API_TOKEN" \ | |
| -e SIGNING_KEY="$SIGNING_KEY" \ | |
| -e DSTACK_DOCKER_REGISTRY="ghcr.io" \ | |
| -e DSTACK_DOCKER_USERNAME="$GHCR_USERNAME" \ | |
| -e DSTACK_DOCKER_PASSWORD="$GHCR_TOKEN" \ | |
| 2>&1 | tee deploy.log | |
| echo "deployment_time=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_OUTPUT | |
| - name: Skip deployment notice | |
| if: steps.check.outputs.skip_deploy == 'true' | |
| run: | | |
| echo "## Deployment Skipped" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "The compose hash has not changed since the last deployment." >> $GITHUB_STEP_SUMMARY | |
| echo "Current deployment is already running the latest image." >> $GITHUB_STEP_SUMMARY | |
| - name: Get deployment info | |
| if: steps.check.outputs.skip_deploy != 'true' | |
| env: | |
| PHALA_CLOUD_API_KEY: ${{ secrets.PHALA_CLOUD_API_KEY }} | |
| run: | | |
| echo "Getting deployment status..." | |
| phala cvms get toy-example-staging --json 2>&1 | tee deployment-info.json || true | |
| - name: Record deployment | |
| if: steps.check.outputs.skip_deploy != 'true' | |
| run: | | |
| echo "## Deployment Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "| Item | Value |" >> $GITHUB_STEP_SUMMARY | |
| echo "|------|-------|" >> $GITHUB_STEP_SUMMARY | |
| echo "| Image | \`${{ steps.vars.outputs.enclave_image }}:${{ needs.build-enclave.outputs.sha_short }}\` |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Compose Hash | \`${{ steps.hash.outputs.compose_hash }}\` |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Deploy Time | ${{ steps.deploy.outputs.deployment_time }} |" >> $GITHUB_STEP_SUMMARY | |
| # Multi-machine release deployment (triggered by version tags) | |
| deploy-release: | |
| runs-on: ubuntu-latest | |
| needs: [summary, build-enclave] | |
| if: | | |
| startsWith(github.ref, 'refs/tags/v') && | |
| needs.summary.outputs.all_success == 'true' | |
| environment: production | |
| strategy: | |
| matrix: | |
| include: | |
| - cluster: prod5 | |
| node_id_secret: TOY_NODE_ID_PROD5 | |
| - cluster: prod9 | |
| node_id_secret: TOY_NODE_ID_PROD9 | |
| fail-fast: false | |
| max-parallel: 1 # Deploy sequentially to avoid race conditions | |
| outputs: | |
| prod5_tx: ${{ steps.output.outputs.prod5_tx }} | |
| prod5_hash: ${{ steps.output.outputs.prod5_hash }} | |
| prod5_time: ${{ steps.output.outputs.prod5_time }} | |
| prod9_tx: ${{ steps.output.outputs.prod9_tx }} | |
| prod9_hash: ${{ steps.output.outputs.prod9_hash }} | |
| prod9_time: ${{ steps.output.outputs.prod9_time }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Set variables | |
| id: vars | |
| run: | | |
| OWNER_LC=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') | |
| echo "enclave_image=ghcr.io/${OWNER_LC}/toy-example-enclave" >> $GITHUB_OUTPUT | |
| echo "version=${{ needs.summary.outputs.version }}" >> $GITHUB_OUTPUT | |
| echo "timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_OUTPUT | |
| - name: Update docker-compose with image tag | |
| run: | | |
| cd toy-example-app/enclave | |
| # Update image tag only - cluster is selected via --node-id | |
| sed -i "s|image:.*toy-example-enclave.*|image: ${{ steps.vars.outputs.enclave_image }}:${{ needs.build-enclave.outputs.sha_short }}|g" docker-compose.yml | |
| echo "Updated docker-compose.yml:" | |
| cat docker-compose.yml | |
| - name: Calculate compose hash | |
| id: hash | |
| run: | | |
| HASH=$(sha256sum toy-example-app/enclave/docker-compose.yml | cut -d' ' -f1) | |
| echo "compose_hash=$HASH" >> $GITHUB_OUTPUT | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '20' | |
| - name: Install Phala CLI | |
| run: npm install -g phala | |
| - name: Check if CVM exists | |
| id: check_cvm | |
| env: | |
| PHALA_CLOUD_API_KEY: ${{ secrets.PHALA_CLOUD_API_KEY }} | |
| run: | | |
| CVM_NAME="toy-example-${{ matrix.cluster }}" | |
| if phala cvms get "$CVM_NAME" --json 2>/dev/null | jq -e '.success == true' > /dev/null 2>&1; then | |
| echo "cvm_exists=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "cvm_exists=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Deploy to ${{ matrix.cluster }} | |
| id: deploy | |
| env: | |
| PHALA_CLOUD_API_KEY: ${{ secrets.PHALA_CLOUD_API_KEY }} | |
| MOCK_API_URL: ${{ secrets.TOY_MOCK_API_URL }} | |
| MOCK_API_TOKEN: ${{ secrets.TOY_MOCK_API_TOKEN }} | |
| SIGNING_KEY: ${{ secrets.TOY_SIGNING_KEY }} | |
| GHCR_USERNAME: ${{ github.repository_owner }} | |
| GHCR_TOKEN: ${{ secrets.GHCR_PAT }} | |
| PRIVATE_KEY: ${{ secrets.TOY_DEPLOYER_PRIVATE_KEY }} | |
| NODE_ID_PROD5: ${{ secrets.TOY_NODE_ID_PROD5 }} | |
| NODE_ID_PROD9: ${{ secrets.TOY_NODE_ID_PROD9 }} | |
| run: | | |
| cd toy-example-app/enclave | |
| CVM_NAME="toy-example-${{ matrix.cluster }}" | |
| # Select node ID based on cluster | |
| if [ "${{ matrix.cluster }}" = "prod5" ]; then | |
| NODE_ID="$NODE_ID_PROD5" | |
| else | |
| NODE_ID="$NODE_ID_PROD9" | |
| fi | |
| # Use --cvm-id to upgrade existing CVM, or create new if doesn't exist | |
| if [ "${{ steps.check_cvm.outputs.cvm_exists }}" = "true" ]; then | |
| echo "Upgrading existing CVM on ${{ matrix.cluster }}..." | |
| DEPLOY_ARGS="--cvm-id $CVM_NAME --wait" | |
| else | |
| echo "Creating new CVM on ${{ matrix.cluster }}..." | |
| # Note: --custom-app-id and --nonce only work with PHALA KMS, not Base KMS | |
| # Each deployment will get its own app_id from Base KMS | |
| DEPLOY_ARGS="-n $CVM_NAME --node-id $NODE_ID" | |
| fi | |
| # Deploy/upgrade with Base KMS for on-chain transparency logging | |
| phala deploy \ | |
| -c docker-compose.yml \ | |
| $DEPLOY_ARGS \ | |
| --kms base \ | |
| --private-key "$PRIVATE_KEY" \ | |
| -e MOCK_API_URL="$MOCK_API_URL" \ | |
| -e MOCK_API_TOKEN="$MOCK_API_TOKEN" \ | |
| -e SIGNING_KEY="$SIGNING_KEY" \ | |
| -e ENVIRONMENT="${{ matrix.cluster }}" \ | |
| -e DSTACK_DOCKER_REGISTRY="ghcr.io" \ | |
| -e DSTACK_DOCKER_USERNAME="$GHCR_USERNAME" \ | |
| -e DSTACK_DOCKER_PASSWORD="$GHCR_TOKEN" \ | |
| 2>&1 | tee deploy-${{ matrix.cluster }}.log | |
| # Try to extract TX hash from deploy output | |
| TX_HASH=$(grep -oE '0x[a-fA-F0-9]{64}' deploy-${{ matrix.cluster }}.log | head -1 || echo "pending") | |
| echo "tx_hash=$TX_HASH" >> $GITHUB_OUTPUT | |
| echo "deployment_time=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_OUTPUT | |
| - name: Save deployment info for consolidation | |
| run: | | |
| # Write deployment info to a file for artifact upload | |
| mkdir -p deploy-info | |
| cat > deploy-info/${{ matrix.cluster }}.json <<EOF | |
| { | |
| "cluster": "${{ matrix.cluster }}", | |
| "tx_hash": "${{ steps.deploy.outputs.tx_hash }}", | |
| "compose_hash": "${{ steps.hash.outputs.compose_hash }}", | |
| "timestamp": "${{ steps.vars.outputs.timestamp }}", | |
| "version": "${{ steps.vars.outputs.version }}" | |
| } | |
| EOF | |
| cat deploy-info/${{ matrix.cluster }}.json | |
| - name: Upload deployment info | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: deploy-info-${{ matrix.cluster }} | |
| path: deploy-info/${{ matrix.cluster }}.json | |
| retention-days: 7 | |
| - name: Record release deployment | |
| run: | | |
| echo "## Release Deployment - ${{ matrix.cluster }}" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "| Item | Value |" >> $GITHUB_STEP_SUMMARY | |
| echo "|------|-------|" >> $GITHUB_STEP_SUMMARY | |
| echo "| Version | \`${{ steps.vars.outputs.version }}\` |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Cluster | ${{ matrix.cluster }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Image | \`${{ steps.vars.outputs.enclave_image }}:${{ needs.build-enclave.outputs.sha_short }}\` |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Compose Hash | \`${{ steps.hash.outputs.compose_hash }}\` |" >> $GITHUB_STEP_SUMMARY | |
| echo "| On-Chain TX | ${{ steps.deploy.outputs.tx_hash }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Deploy Time | ${{ steps.deploy.outputs.deployment_time }} |" >> $GITHUB_STEP_SUMMARY | |
| # Consolidate deployment records after all machines are deployed | |
| record-deployments: | |
| runs-on: ubuntu-latest | |
| needs: [summary, build-enclave, deploy-release] | |
| if: | | |
| startsWith(github.ref, 'refs/tags/v') && | |
| always() && | |
| (needs.deploy-release.result == 'success' || needs.deploy-release.result == 'failure') | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: feat/toy-example-app | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Set variables | |
| id: vars | |
| run: | | |
| OWNER_LC=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') | |
| echo "enclave_image=ghcr.io/${OWNER_LC}/toy-example-enclave" >> $GITHUB_OUTPUT | |
| echo "version=${{ needs.summary.outputs.version }}" >> $GITHUB_OUTPUT | |
| echo "timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_OUTPUT | |
| - name: Download deployment artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: deploy-info-* | |
| path: deploy-artifacts | |
| merge-multiple: false | |
| - name: List downloaded artifacts | |
| run: | | |
| echo "Downloaded artifacts:" | |
| find deploy-artifacts -type f -name "*.json" -exec cat {} \; | |
| - name: Update DEPLOYMENTS.md | |
| run: | | |
| # Use absolute paths to avoid directory navigation issues | |
| ROOT_DIR="${GITHUB_WORKSPACE}" | |
| DEPLOYMENTS_FILE="${ROOT_DIR}/toy-example-app/DEPLOYMENTS.md" | |
| ENCLAVE_DIR="${ROOT_DIR}/toy-example-app/enclave" | |
| VERSION="${{ steps.vars.outputs.version }}" | |
| TIMESTAMP="${{ steps.vars.outputs.timestamp }}" | |
| OWNER_LC=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') | |
| SHA_SHORT="${{ needs.build-enclave.outputs.sha_short }}" | |
| # Create file if it doesn't exist | |
| if [ ! -f "$DEPLOYMENTS_FILE" ]; then | |
| { | |
| echo "# Deployment History" | |
| echo "" | |
| echo "Auto-generated deployment log. Each entry represents a verified deployment to Phala Cloud with on-chain transparency logging via Base KMS." | |
| echo "" | |
| echo "## Active Deployments" | |
| echo "" | |
| echo "| Timestamp | Version | Machine | Compose Hash | On-Chain TX | Status |" | |
| echo "|-----------|---------|---------|--------------|-------------|--------|" | |
| } > "$DEPLOYMENTS_FILE" | |
| fi | |
| # Calculate compose hash once - same for all clusters since we use --node-id | |
| git -C "${ENCLAVE_DIR}" checkout docker-compose.yml | |
| sed -i "s|image:.*toy-example-enclave.*|image: ghcr.io/${OWNER_LC}/toy-example-enclave:${SHA_SHORT}|g" "${ENCLAVE_DIR}/docker-compose.yml" | |
| HASH=$(sha256sum "${ENCLAVE_DIR}/docker-compose.yml" | cut -d' ' -f1) | |
| # Add entries for each cluster - same hash proves identical code | |
| for CLUSTER in prod5 prod9; do | |
| # Read TX hash from artifact if available | |
| ARTIFACT_FILE="${ROOT_DIR}/deploy-artifacts/deploy-info-${CLUSTER}/${CLUSTER}.json" | |
| if [ -f "$ARTIFACT_FILE" ]; then | |
| TX_HASH=$(jq -r '.tx_hash // "pending"' "$ARTIFACT_FILE") | |
| echo "Found TX hash for ${CLUSTER}: ${TX_HASH}" | |
| else | |
| TX_HASH="pending" | |
| echo "No artifact found for ${CLUSTER}, using pending" | |
| fi | |
| # Format TX as link if it's a valid hash | |
| if [[ "$TX_HASH" =~ ^0x[a-fA-F0-9]{64}$ ]]; then | |
| TX_LINK="[View](https://basescan.org/tx/${TX_HASH})" | |
| else | |
| TX_LINK="pending" | |
| fi | |
| NEW_ENTRY="| ${TIMESTAMP} | ${VERSION} | ${CLUSTER} | \`${HASH}\` | ${TX_LINK} | Active |" | |
| # Insert after the header separator row | |
| sed -i "/^|-----------|/a ${NEW_ENTRY}" "$DEPLOYMENTS_FILE" | |
| done | |
| # Reset docker-compose.yml | |
| git -C "${ENCLAVE_DIR}" checkout docker-compose.yml | |
| echo "Updated DEPLOYMENTS.md:" | |
| cat "$DEPLOYMENTS_FILE" | |
| - name: Commit DEPLOYMENTS.md | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add toy-example-app/DEPLOYMENTS.md | |
| git commit -m "chore: record deployments v${{ steps.vars.outputs.version }} | |
| Version: ${{ steps.vars.outputs.version }} | |
| Machines: prod5, prod9 | |
| Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>" || echo "No changes to commit" | |
| git push origin feat/toy-example-app | |
| - name: Record consolidation summary | |
| run: | | |
| echo "## Deployment Records Consolidated" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "DEPLOYMENTS.md has been updated with entries for all deployed machines." >> $GITHUB_STEP_SUMMARY |