Deploy Soroban Contracts #25
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
| name: Deploy Soroban Contracts | |
| # --------------------------------------------------------------------------- | |
| # Triggers | |
| # --------------------------------------------------------------------------- | |
| # • Testnet: runs automatically after CI passes on main (push event). | |
| # • Mainnet: MANUAL workflow_dispatch only — requires explicit human action. | |
| # Deploying to mainnet costs real XLM and produces immutable addresses. | |
| # | |
| # Required GitHub secrets (Settings → Secrets → Actions): | |
| # SOROBAN_DEPLOYER_SECRET Stellar secret key (S...) of the deployer account | |
| # SOROBAN_ADMIN_ADDRESS Stellar public key (G...) to set as contract admin | |
| # | |
| # Optional (falls back to defaults if not set): | |
| # SOROBAN_RPC_URL Override the RPC endpoint for the selected network | |
| on: | |
| # Automatic testnet deploy after CI succeeds on main | |
| workflow_run: | |
| workflows: ["CI"] | |
| branches: [main] | |
| types: [completed] | |
| # Manual dispatch — used for BOTH testnet re-deploys and mainnet deploys. | |
| # Mainnet is gated behind the `mainnet` GitHub Environment which requires | |
| # a manual approval from a maintainer before the job runs. | |
| workflow_dispatch: | |
| inputs: | |
| network: | |
| description: "Target network" | |
| required: true | |
| default: testnet | |
| type: choice | |
| options: | |
| - testnet | |
| - mainnet | |
| contract: | |
| description: "Which contract to deploy" | |
| required: true | |
| default: all | |
| type: choice | |
| options: | |
| - all | |
| - escrow | |
| - marketplace | |
| initialize: | |
| description: "Call initialize() after deployment?" | |
| required: true | |
| default: "true" | |
| type: choice | |
| options: | |
| - "true" | |
| - "false" | |
| # One deploy at a time per network to prevent race conditions on deployments.json | |
| concurrency: | |
| group: soroban-deploy-${{ github.event.inputs.network || 'testnet' }} | |
| cancel-in-progress: false | |
| # --------------------------------------------------------------------------- | |
| # Jobs | |
| # --------------------------------------------------------------------------- | |
| jobs: | |
| # ── Guard: only proceed if CI passed (for the workflow_run trigger) ─────── | |
| check-ci: | |
| name: Guard — CI must have passed | |
| runs-on: ubuntu-latest | |
| # For manual dispatch, always proceed. For workflow_run, only proceed on success. | |
| if: >- | |
| github.event_name == 'workflow_dispatch' || | |
| github.event.workflow_run.conclusion == 'success' | |
| steps: | |
| - name: CI status OK | |
| run: echo "CI passed — proceeding with deploy." | |
| # ── Testnet deploy ───────────────────────────────────────────────────────── | |
| deploy-testnet: | |
| name: Deploy to Testnet | |
| needs: check-ci | |
| runs-on: ubuntu-latest | |
| # Run on: automatic testnet trigger, or manual dispatch targeting testnet | |
| if: >- | |
| github.event_name == 'workflow_run' || | |
| (github.event_name == 'workflow_dispatch' && github.event.inputs.network == 'testnet') | |
| environment: testnet | |
| permissions: | |
| contents: write # needed to commit deployments.json | |
| outputs: | |
| escrow_id: ${{ steps.deploy-escrow.outputs.contract_id }} | |
| marketplace_id: ${{ steps.deploy-marketplace.outputs.contract_id }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| # ── Rust + WASM target ──────────────────────────────────────────────── | |
| - name: Set up Rust toolchain | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: wasm32v1-none | |
| - name: Cache Cargo | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| contracts/target | |
| key: cargo-deploy-${{ hashFiles('contracts/Cargo.lock') }} | |
| - name: Install stellar-cli | |
| run: cargo install --locked stellar-cli | |
| # ── Build ───────────────────────────────────────────────────────────── | |
| - name: Build WASM (release) | |
| working-directory: contracts | |
| run: cargo build --release --target wasm32v1-none | |
| # ── Configure network identity ──────────────────────────────────────── | |
| - name: Add deployer identity | |
| env: | |
| SOROBAN_DEPLOYER_SECRET: ${{ secrets.SOROBAN_DEPLOYER_SECRET }} | |
| run: | | |
| echo "$SOROBAN_DEPLOYER_SECRET" | \ | |
| stellar keys add deployer --secret-key --stdin | |
| - name: Configure testnet | |
| env: | |
| SOROBAN_RPC_URL: ${{ vars.SOROBAN_RPC_URL }} | |
| run: | | |
| stellar network add testnet \ | |
| --rpc-url "${SOROBAN_RPC_URL:-https://soroban-testnet.stellar.org}" \ | |
| --network-passphrase "Test SDF Network ; September 2015" | |
| # ── Deploy escrow ───────────────────────────────────────────────────── | |
| - name: Deploy escrow contract | |
| id: deploy-escrow | |
| # Deploy escrow when: all contracts requested, or escrow specifically | |
| if: >- | |
| github.event_name == 'workflow_run' || | |
| github.event.inputs.contract == 'all' || | |
| github.event.inputs.contract == 'escrow' | |
| run: | | |
| CONTRACT_ID=$(stellar contract deploy \ | |
| --wasm contracts/target/wasm32v1-none/release/airflex_escrow.wasm \ | |
| --source deployer \ | |
| --network testnet) | |
| echo "contract_id=$CONTRACT_ID" >> "$GITHUB_OUTPUT" | |
| echo "Deployed escrow: $CONTRACT_ID" | |
| - name: Initialize escrow contract | |
| if: >- | |
| steps.deploy-escrow.conclusion == 'success' && | |
| (github.event_name == 'workflow_run' || github.event.inputs.initialize == 'true') | |
| env: | |
| SOROBAN_ADMIN_ADDRESS: ${{ secrets.SOROBAN_ADMIN_ADDRESS }} | |
| run: | | |
| stellar contract invoke \ | |
| --id ${{ steps.deploy-escrow.outputs.contract_id }} \ | |
| --source deployer \ | |
| --network testnet \ | |
| -- initialize \ | |
| --admin "$SOROBAN_ADMIN_ADDRESS" | |
| # ── Deploy marketplace ──────────────────────────────────────────────── | |
| - name: Deploy marketplace contract | |
| id: deploy-marketplace | |
| if: >- | |
| github.event_name == 'workflow_run' || | |
| github.event.inputs.contract == 'all' || | |
| github.event.inputs.contract == 'marketplace' | |
| run: | | |
| CONTRACT_ID=$(stellar contract deploy \ | |
| --wasm contracts/target/wasm32v1-none/release/airflex_marketplace.wasm \ | |
| --source deployer \ | |
| --network testnet) | |
| echo "contract_id=$CONTRACT_ID" >> "$GITHUB_OUTPUT" | |
| echo "Deployed marketplace: $CONTRACT_ID" | |
| - name: Initialize marketplace contract | |
| if: >- | |
| steps.deploy-marketplace.conclusion == 'success' && | |
| (github.event_name == 'workflow_run' || github.event.inputs.initialize == 'true') | |
| env: | |
| SOROBAN_ADMIN_ADDRESS: ${{ secrets.SOROBAN_ADMIN_ADDRESS }} | |
| run: | | |
| stellar contract invoke \ | |
| --id ${{ steps.deploy-marketplace.outputs.contract_id }} \ | |
| --source deployer \ | |
| --network testnet \ | |
| -- initialize \ | |
| --admin "$SOROBAN_ADMIN_ADDRESS" | |
| # ── Update deployments.json ─────────────────────────────────────────── | |
| - name: Update deployments.json | |
| run: | | |
| node -e " | |
| const fs = require('fs'); | |
| const path = 'contracts/deployments.json'; | |
| const data = JSON.parse(fs.readFileSync(path, 'utf8')); | |
| const escrowId = '${{ steps.deploy-escrow.outputs.contract_id }}'; | |
| const marketId = '${{ steps.deploy-marketplace.outputs.contract_id }}'; | |
| if (escrowId) data.testnet.escrow = escrowId; | |
| if (marketId) data.testnet.marketplace = marketId; | |
| fs.writeFileSync(path, JSON.stringify(data, null, 2) + '\n'); | |
| console.log('deployments.json updated'); | |
| console.log(JSON.stringify(data.testnet, null, 2)); | |
| " | |
| - name: Commit deployments.json | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add contracts/deployments.json | |
| if git diff --cached --quiet; then | |
| echo "No changes to deployments.json — addresses unchanged." | |
| else | |
| git commit -m "chore(contracts): update testnet deployment addresses [skip ci]" | |
| git push | |
| fi | |
| # ── Summary ─────────────────────────────────────────────────────────── | |
| - name: Print deployment summary | |
| run: | | |
| ESCROW_ID="${{ steps.deploy-escrow.outputs.contract_id }}" | |
| MARKET_ID="${{ steps.deploy-marketplace.outputs.contract_id }}" | |
| echo "### ✅ Testnet Contracts Deployed" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Contract | Contract ID | Explorer |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "|----------|-------------|----------|" >> "$GITHUB_STEP_SUMMARY" | |
| if [ -n "$ESCROW_ID" ]; then | |
| echo "| Escrow | \`$ESCROW_ID\` | [Stellar Expert](https://stellar.expert/explorer/testnet/contract/$ESCROW_ID) |" >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| if [ -n "$MARKET_ID" ]; then | |
| echo "| Marketplace | \`$MARKET_ID\` | [Stellar Expert](https://stellar.expert/explorer/testnet/contract/$MARKET_ID) |" >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Field | Value |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "|-------|-------|" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Network | testnet |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Commit | \`${{ github.sha }}\` |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "**Next step:** Update \`ESCROW_CONTRACT_ID\` and \`MARKETPLACE_CONTRACT_ID\` in Railway." >> "$GITHUB_STEP_SUMMARY" | |
| # ── Mainnet deploy (MANUAL ONLY) ────────────────────────────────────────── | |
| deploy-mainnet: | |
| name: Deploy to Mainnet | |
| needs: check-ci | |
| runs-on: ubuntu-latest | |
| # ONLY runs on manual dispatch targeting mainnet. | |
| # The `mainnet` GitHub Environment MUST have a required reviewer configured | |
| # (Settings → Environments → mainnet → Required reviewers). | |
| if: >- | |
| github.event_name == 'workflow_dispatch' && | |
| github.event.inputs.network == 'mainnet' | |
| environment: mainnet # enforces manual approval gate | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Set up Rust toolchain | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: wasm32v1-none | |
| - name: Cache Cargo | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| contracts/target | |
| key: cargo-deploy-${{ hashFiles('contracts/Cargo.lock') }} | |
| - name: Install stellar-cli | |
| run: cargo install --locked stellar-cli | |
| - name: Build WASM (release) | |
| working-directory: contracts | |
| run: cargo build --release --target wasm32v1-none | |
| - name: Add deployer identity | |
| env: | |
| SOROBAN_DEPLOYER_SECRET: ${{ secrets.SOROBAN_DEPLOYER_SECRET }} | |
| run: | | |
| echo "$SOROBAN_DEPLOYER_SECRET" | \ | |
| stellar keys add deployer --secret-key --stdin | |
| - name: Configure mainnet | |
| env: | |
| SOROBAN_RPC_URL: ${{ vars.SOROBAN_RPC_URL }} | |
| run: | | |
| stellar network add mainnet \ | |
| --rpc-url "${SOROBAN_RPC_URL:-https://mainnet.stellar.validationcloud.io/v1/}" \ | |
| --network-passphrase "Public Global Stellar Network ; September 2015" | |
| - name: Deploy escrow contract (mainnet) | |
| id: deploy-escrow-mainnet | |
| if: >- | |
| github.event.inputs.contract == 'all' || | |
| github.event.inputs.contract == 'escrow' | |
| run: | | |
| CONTRACT_ID=$(stellar contract deploy \ | |
| --wasm contracts/target/wasm32v1-none/release/airflex_escrow.wasm \ | |
| --source deployer \ | |
| --network mainnet) | |
| echo "contract_id=$CONTRACT_ID" >> "$GITHUB_OUTPUT" | |
| echo "Deployed escrow (mainnet): $CONTRACT_ID" | |
| - name: Initialize escrow (mainnet) | |
| if: >- | |
| steps.deploy-escrow-mainnet.conclusion == 'success' && | |
| github.event.inputs.initialize == 'true' | |
| env: | |
| SOROBAN_ADMIN_ADDRESS: ${{ secrets.SOROBAN_ADMIN_ADDRESS }} | |
| run: | | |
| stellar contract invoke \ | |
| --id ${{ steps.deploy-escrow-mainnet.outputs.contract_id }} \ | |
| --source deployer \ | |
| --network mainnet \ | |
| -- initialize \ | |
| --admin "$SOROBAN_ADMIN_ADDRESS" | |
| - name: Deploy marketplace contract (mainnet) | |
| id: deploy-marketplace-mainnet | |
| if: >- | |
| github.event.inputs.contract == 'all' || | |
| github.event.inputs.contract == 'marketplace' | |
| run: | | |
| CONTRACT_ID=$(stellar contract deploy \ | |
| --wasm contracts/target/wasm32v1-none/release/airflex_marketplace.wasm \ | |
| --source deployer \ | |
| --network mainnet) | |
| echo "contract_id=$CONTRACT_ID" >> "$GITHUB_OUTPUT" | |
| echo "Deployed marketplace (mainnet): $CONTRACT_ID" | |
| - name: Initialize marketplace (mainnet) | |
| if: >- | |
| steps.deploy-marketplace-mainnet.conclusion == 'success' && | |
| github.event.inputs.initialize == 'true' | |
| env: | |
| SOROBAN_ADMIN_ADDRESS: ${{ secrets.SOROBAN_ADMIN_ADDRESS }} | |
| run: | | |
| stellar contract invoke \ | |
| --id ${{ steps.deploy-marketplace-mainnet.outputs.contract_id }} \ | |
| --source deployer \ | |
| --network mainnet \ | |
| -- initialize \ | |
| --admin "$SOROBAN_ADMIN_ADDRESS" | |
| - name: Update deployments.json (mainnet) | |
| run: | | |
| node -e " | |
| const fs = require('fs'); | |
| const path = 'contracts/deployments.json'; | |
| const data = JSON.parse(fs.readFileSync(path, 'utf8')); | |
| const escrowId = '${{ steps.deploy-escrow-mainnet.outputs.contract_id }}'; | |
| const marketId = '${{ steps.deploy-marketplace-mainnet.outputs.contract_id }}'; | |
| if (escrowId) data.mainnet.escrow = escrowId; | |
| if (marketId) data.mainnet.marketplace = marketId; | |
| fs.writeFileSync(path, JSON.stringify(data, null, 2) + '\n'); | |
| console.log('deployments.json updated (mainnet)'); | |
| " | |
| - name: Commit deployments.json (mainnet) | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add contracts/deployments.json | |
| if git diff --cached --quiet; then | |
| echo "No changes to commit." | |
| else | |
| git commit -m "chore(contracts): update mainnet deployment addresses [skip ci]" | |
| git push | |
| fi | |
| - name: Print mainnet deployment summary | |
| run: | | |
| ESCROW_ID="${{ steps.deploy-escrow-mainnet.outputs.contract_id }}" | |
| MARKET_ID="${{ steps.deploy-marketplace-mainnet.outputs.contract_id }}" | |
| echo "### ✅ Mainnet Contracts Deployed" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Contract | Contract ID | Explorer |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "|----------|-------------|----------|" >> "$GITHUB_STEP_SUMMARY" | |
| if [ -n "$ESCROW_ID" ]; then | |
| echo "| Escrow | \`$ESCROW_ID\` | [Stellar Expert](https://stellar.expert/explorer/public/contract/$ESCROW_ID) |" >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| if [ -n "$MARKET_ID" ]; then | |
| echo "| Marketplace | \`$MARKET_ID\` | [Stellar Expert](https://stellar.expert/explorer/public/contract/$MARKET_ID) |" >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Field | Value |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "|-------|-------|" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Network | **mainnet** |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Triggered by | @${{ github.actor }} |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Commit | \`${{ github.sha }}\` |" >> "$GITHUB_STEP_SUMMARY" |