Create Devnet #38
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: Create Devnet | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| devnet_name: | |
| description: "Devnet name (without 'devnet-' prefix, e.g. 'mytest' creates 'devnet-mytest')" | |
| required: true | |
| type: string | |
| platform_version: | |
| description: "Platform/dashmate version to deploy (e.g. 2.0.0-rc.16)" | |
| required: true | |
| type: string | |
| default: "2.0.0-rc.16" | |
| deploy_tags: | |
| description: "Ansible tags to run. Use full_deploy for full flow, or a narrower tag such as unban_hp_masternodes to resume faster." | |
| required: true | |
| type: string | |
| default: "full_deploy" | |
| # Advanced options - sane defaults, only change if you know what you're doing | |
| hp_masternodes_arm_count: | |
| description: "Advanced: Number of ARM HP masternodes" | |
| required: false | |
| type: string | |
| default: "11" | |
| hp_masternodes_amd_count: | |
| description: "Advanced: Number of AMD HP masternodes" | |
| required: false | |
| type: string | |
| default: "0" | |
| masternodes_arm_count: | |
| description: "Advanced: Number of ARM regular masternodes" | |
| required: false | |
| type: string | |
| default: "0" | |
| masternodes_amd_count: | |
| description: "Advanced: Number of AMD regular masternodes" | |
| required: false | |
| type: string | |
| default: "0" | |
| seed_count: | |
| description: "Advanced: Number of seed nodes" | |
| required: false | |
| type: string | |
| default: "1" | |
| core_version: | |
| description: "Advanced: Core (dashd) image version (leave empty for default)" | |
| required: false | |
| type: string | |
| default: "" | |
| hpmn_disk_size: | |
| description: "Advanced: HP masternode disk size in GB" | |
| required: false | |
| type: string | |
| default: "30" | |
| jobs: | |
| create: | |
| name: Create Devnet | |
| runs-on: ubuntu-22.04 | |
| timeout-minutes: 120 | |
| concurrency: | |
| group: "devnet-${{ github.event.inputs.devnet_name }}" | |
| cancel-in-progress: false | |
| env: | |
| NETWORK_NAME: "devnet-${{ github.event.inputs.devnet_name }}" | |
| AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} | |
| AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | |
| AWS_REGION: ${{ secrets.AWS_REGION }} | |
| TERRAFORM_S3_BUCKET: ${{ secrets.TERRAFORM_S3_BUCKET }} | |
| TERRAFORM_S3_KEY: ${{ secrets.TERRAFORM_S3_KEY }} | |
| TERRAFORM_DYNAMODB_TABLE: ${{ secrets.TERRAFORM_DYNAMODB_TABLE }} | |
| ANSIBLE_HOST_KEY_CHECKING: "false" | |
| steps: | |
| - name: Validate devnet name | |
| env: | |
| NAME: ${{ github.event.inputs.devnet_name }} | |
| run: | | |
| if [[ -z "$NAME" ]]; then | |
| echo "Error: devnet_name is required" | |
| exit 1 | |
| fi | |
| if [[ "$NAME" =~ ^devnet- ]]; then | |
| echo "Error: Do not include 'devnet-' prefix. Just provide the name (e.g. 'mytest')" | |
| exit 1 | |
| fi | |
| if [[ ! "$NAME" =~ ^[a-z0-9][a-z0-9-]*$ ]]; then | |
| echo "Error: devnet_name must be lowercase alphanumeric with optional hyphens" | |
| exit 1 | |
| fi | |
| echo "Will create: devnet-$NAME" | |
| - name: Checkout dash-network-deploy | |
| uses: actions/checkout@v4 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '20' | |
| - name: Install Node.js dependencies | |
| run: npm ci | |
| - name: Set up Terraform | |
| uses: hashicorp/setup-terraform@v3 | |
| with: | |
| terraform_version: "1.12.1" | |
| terraform_wrapper: false | |
| - name: Install system dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y python3-pip python3-netaddr sshpass jq | |
| - name: Install Ansible | |
| run: | | |
| python3 -m pip install --upgrade pip | |
| python3 -m pip install ansible-core==2.16.3 jmespath | |
| - name: Install Ansible roles | |
| run: | | |
| ansible-galaxy install -r ansible/requirements.yml | |
| mkdir -p ~/.ansible/roles | |
| cp -r ansible/roles/* ~/.ansible/roles/ | |
| - name: Set up SSH keys | |
| env: | |
| DEPLOY_SERVER_KEY: ${{ secrets.DEPLOY_SERVER_KEY }} | |
| EVO_APP_DEPLOY_KEY: ${{ secrets.EVO_APP_DEPLOY_KEY }} | |
| EVO_APP_DEPLOY_WRITE_KEY: ${{ secrets.EVO_APP_DEPLOY_WRITE_KEY }} | |
| run: | | |
| mkdir -p ~/.ssh | |
| # Server SSH key for connecting to nodes | |
| printf '%s\n' "$DEPLOY_SERVER_KEY" > ~/.ssh/id_rsa | |
| chmod 600 ~/.ssh/id_rsa | |
| # Derive public key from private key | |
| ssh-keygen -y -f ~/.ssh/id_rsa > ~/.ssh/id_rsa.pub | |
| chmod 644 ~/.ssh/id_rsa.pub | |
| # GitHub deploy key for cloning configs repo | |
| printf '%s\n' "$EVO_APP_DEPLOY_KEY" > ~/.ssh/id_ed25519 | |
| chmod 600 ~/.ssh/id_ed25519 | |
| # Optional write key for pushing to configs repo | |
| if [[ -n "$EVO_APP_DEPLOY_WRITE_KEY" ]]; then | |
| printf '%s\n' "$EVO_APP_DEPLOY_WRITE_KEY" > ~/.ssh/id_ed25519_write | |
| chmod 600 ~/.ssh/id_ed25519_write | |
| fi | |
| # SSH config | |
| cat > ~/.ssh/config << 'EOL' | |
| Host github.com | |
| IdentityFile ~/.ssh/id_ed25519 | |
| StrictHostKeyChecking no | |
| Host * | |
| IdentityFile ~/.ssh/id_rsa | |
| User ubuntu | |
| StrictHostKeyChecking no | |
| UserKnownHostsFile=/dev/null | |
| EOL | |
| chmod 600 ~/.ssh/config | |
| - name: Create networks/.env | |
| run: | | |
| mkdir -p networks | |
| cat > networks/.env << EOF | |
| PRIVATE_KEY_PATH=$HOME/.ssh/id_rsa | |
| PUBLIC_KEY_PATH=$HOME/.ssh/id_rsa.pub | |
| AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID | |
| AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY | |
| AWS_REGION=$AWS_REGION | |
| TERRAFORM_S3_BUCKET=$TERRAFORM_S3_BUCKET | |
| TERRAFORM_S3_KEY=$TERRAFORM_S3_KEY | |
| TERRAFORM_DYNAMODB_TABLE=$TERRAFORM_DYNAMODB_TABLE | |
| EOF | |
| - name: Check for existing devnet configs | |
| id: existing_configs | |
| run: | | |
| git clone git@github.com:dashpay/dash-network-configs.git /tmp/dash-network-configs-source | |
| FOUND=0 | |
| MISSING=0 | |
| for ext in yml tfvars inventory; do | |
| SRC="/tmp/dash-network-configs-source/$NETWORK_NAME.$ext" | |
| if [[ -f "$SRC" ]]; then | |
| FOUND=$((FOUND + 1)) | |
| else | |
| MISSING=$((MISSING + 1)) | |
| fi | |
| done | |
| if [[ $FOUND -eq 3 ]]; then | |
| echo "resume_mode=true" >> "$GITHUB_OUTPUT" | |
| echo "Found existing config set for $NETWORK_NAME. Reusing config repo files and skipping Terraform." | |
| cp "/tmp/dash-network-configs-source/$NETWORK_NAME.yml" networks/ | |
| cp "/tmp/dash-network-configs-source/$NETWORK_NAME.tfvars" networks/ | |
| cp "/tmp/dash-network-configs-source/$NETWORK_NAME.inventory" networks/ | |
| elif [[ $FOUND -eq 0 ]]; then | |
| echo "resume_mode=false" >> "$GITHUB_OUTPUT" | |
| echo "No existing config set found for $NETWORK_NAME. Running full create flow." | |
| else | |
| echo "Error: Partial config set found for $NETWORK_NAME in dash-network-configs." | |
| ls -la /tmp/dash-network-configs-source/$NETWORK_NAME.* 2>/dev/null || true | |
| exit 1 | |
| fi | |
| - name: Generate network configs | |
| if: steps.existing_configs.outputs.resume_mode != 'true' | |
| env: | |
| MN_AMD: ${{ github.event.inputs.masternodes_amd_count }} | |
| MN_ARM: ${{ github.event.inputs.masternodes_arm_count }} | |
| HP_AMD: ${{ github.event.inputs.hp_masternodes_amd_count }} | |
| HP_ARM: ${{ github.event.inputs.hp_masternodes_arm_count }} | |
| SEED_COUNT: ${{ github.event.inputs.seed_count }} | |
| run: | | |
| # Validate all counts are numeric | |
| for var in MN_AMD MN_ARM HP_AMD HP_ARM SEED_COUNT; do | |
| val="${!var}" | |
| if [[ ! "$val" =~ ^[0-9]+$ ]]; then | |
| echo "Error: $var must be a number, got '$val'" | |
| exit 1 | |
| fi | |
| done | |
| echo "Generating configs for $NETWORK_NAME..." | |
| chmod +x ./bin/generate | |
| ./bin/generate "$NETWORK_NAME" \ | |
| "$MN_AMD" "$MN_ARM" "$HP_AMD" "$HP_ARM" \ | |
| -s="$SEED_COUNT" | |
| echo "Generated config files:" | |
| ls -la networks/devnet-* | |
| - name: Update platform version in config | |
| env: | |
| VERSION: ${{ github.event.inputs.platform_version }} | |
| CORE_VERSION: ${{ github.event.inputs.core_version }} | |
| run: | | |
| CONFIG_FILE="networks/$NETWORK_NAME.yml" | |
| # Escape sed-special characters in version strings | |
| SAFE_VERSION=$(printf '%s' "$VERSION" | sed 's/[&\\/]/\\&/g') | |
| SAFE_CORE_VERSION=$(printf '%s' "$CORE_VERSION" | sed 's/[&\\/]/\\&/g') | |
| echo "Setting platform version to $VERSION..." | |
| # Update dashmate version | |
| sed -i "s/dashmate_version: .*/dashmate_version: $SAFE_VERSION/" "$CONFIG_FILE" | |
| # Update platform service images | |
| sed -i "s|drive_image: dashpay/drive:.*|drive_image: dashpay/drive:$SAFE_VERSION|" "$CONFIG_FILE" | |
| sed -i "s|dapi_image: dashpay/dapi:.*|dapi_image: dashpay/dapi:$SAFE_VERSION|" "$CONFIG_FILE" | |
| # Add rs_dapi_image (not in generated config, but group_vars/all defines it | |
| # without a tag, so we must explicitly set it to get the right version) | |
| if ! grep -q "rs_dapi_image:" "$CONFIG_FILE"; then | |
| echo "rs_dapi_image: dashpay/rs-dapi:$VERSION" >> "$CONFIG_FILE" | |
| else | |
| sed -i "s|rs_dapi_image: dashpay/rs-dapi:.*|rs_dapi_image: dashpay/rs-dapi:$SAFE_VERSION|" "$CONFIG_FILE" | |
| fi | |
| # Update core version if specified | |
| if [[ -n "$CORE_VERSION" ]]; then | |
| echo "Setting core version to $CORE_VERSION..." | |
| sed -i "s|dashd_image: dashpay/dashd:.*|dashd_image: dashpay/dashd:$SAFE_CORE_VERSION|" "$CONFIG_FILE" | |
| fi | |
| echo "Updated config:" | |
| grep -E "(dashmate_version|drive_image|dapi_image|rs_dapi_image|dashd_image)" "$CONFIG_FILE" | |
| - name: Update terraform config | |
| env: | |
| DISK_SIZE: ${{ github.event.inputs.hpmn_disk_size }} | |
| run: | | |
| TFVARS_FILE="networks/$NETWORK_NAME.tfvars" | |
| DEFAULT_MAIN_DOMAIN="networks.dash.org" | |
| # Read current value from file (empty if not set) | |
| CURRENT_SIZE=$(grep -oP 'hpmn_node_disk_size\s*=\s*\K[0-9]+' "$TFVARS_FILE" 2>/dev/null || echo "") | |
| CURRENT_MAIN_DOMAIN=$(grep -oP 'main_domain\s*=\s*"\K[^"]*' "$TFVARS_FILE" 2>/dev/null || echo "") | |
| # Generated tfvars leaves main_domain empty; ensure ACM DNS names are valid. | |
| if [[ -z "$CURRENT_MAIN_DOMAIN" ]]; then | |
| echo "Setting main_domain to $DEFAULT_MAIN_DOMAIN..." | |
| if grep -q '^main_domain\s*=' "$TFVARS_FILE"; then | |
| sed -i "s|^main_domain\\s*=.*|main_domain = \"$DEFAULT_MAIN_DOMAIN\"|" "$TFVARS_FILE" | |
| else | |
| echo "main_domain = \"$DEFAULT_MAIN_DOMAIN\"" >> "$TFVARS_FILE" | |
| fi | |
| fi | |
| if [[ -n "$DISK_SIZE" && "$DISK_SIZE" != "$CURRENT_SIZE" ]]; then | |
| if [[ ! "$DISK_SIZE" =~ ^[0-9]+$ ]]; then | |
| echo "Error: hpmn_disk_size must be a number, got '$DISK_SIZE'" | |
| exit 1 | |
| fi | |
| echo "Setting HP masternode disk size to ${DISK_SIZE}GB..." | |
| if [[ -n "$CURRENT_SIZE" ]]; then | |
| sed -i "s/hpmn_node_disk_size = .*/hpmn_node_disk_size = $DISK_SIZE/" "$TFVARS_FILE" | |
| else | |
| echo "hpmn_node_disk_size = $DISK_SIZE" >> "$TFVARS_FILE" | |
| fi | |
| fi | |
| echo "Terraform config:" | |
| cat "$TFVARS_FILE" | |
| - name: Deploy devnet (Terraform + Ansible) | |
| env: | |
| TF_IN_AUTOMATION: "true" | |
| TF_CLI_ARGS_apply: "-auto-approve" | |
| DEPLOY_TAGS: ${{ github.event.inputs.deploy_tags }} | |
| run: | | |
| echo "============================================" | |
| echo "Deploying $NETWORK_NAME" | |
| echo "Ansible tags: $DEPLOY_TAGS" | |
| echo "============================================" | |
| chmod +x ./bin/deploy | |
| # GitHub Actions checks out a detached HEAD; bypass branch safety check. | |
| if [[ "${{ steps.existing_configs.outputs.resume_mode }}" == "true" ]]; then | |
| echo "Resume mode enabled. Skipping Terraform and re-running provisioning only." | |
| ./bin/deploy -p -f --tags="$DEPLOY_TAGS" "$NETWORK_NAME" | |
| else | |
| ./bin/deploy -f --tags="$DEPLOY_TAGS" "$NETWORK_NAME" | |
| fi | |
| - name: Push configs to dash-network-configs | |
| if: always() | |
| env: | |
| EVO_APP_DEPLOY_WRITE_KEY: ${{ secrets.EVO_APP_DEPLOY_WRITE_KEY }} | |
| run: | | |
| # Clone the configs repo to a temp directory | |
| git clone git@github.com:dashpay/dash-network-configs.git /tmp/dash-network-configs | |
| # Copy generated config files if present | |
| COPIED=0 | |
| for ext in yml tfvars inventory; do | |
| SRC="networks/$NETWORK_NAME.$ext" | |
| if [[ -f "$SRC" ]]; then | |
| cp "$SRC" /tmp/dash-network-configs/ | |
| COPIED=$((COPIED + 1)) | |
| else | |
| echo "Skipping missing file: $SRC" | |
| fi | |
| done | |
| if [[ $COPIED -eq 0 ]]; then | |
| echo "No config files found to push" | |
| exit 0 | |
| fi | |
| # Commit and push | |
| cd /tmp/dash-network-configs | |
| git config user.name "GitHub Actions" | |
| git config user.email "actions@github.com" | |
| git add . | |
| git commit -m "Add configs for $NETWORK_NAME" || echo "No changes to commit" | |
| # Use optional write key if configured; otherwise try default key. | |
| if [[ -n "$EVO_APP_DEPLOY_WRITE_KEY" && -f "$HOME/.ssh/id_ed25519_write" ]]; then | |
| GIT_SSH_COMMAND='ssh -i ~/.ssh/id_ed25519_write -o StrictHostKeyChecking=no' git push || { | |
| echo "::warning::Failed to push configs with EVO_APP_DEPLOY_WRITE_KEY" | |
| exit 0 | |
| } | |
| else | |
| git push || { | |
| echo "::warning::Failed to push configs (likely read-only EVO_APP_DEPLOY_KEY). Configure secret EVO_APP_DEPLOY_WRITE_KEY with write access." | |
| exit 0 | |
| } | |
| fi | |
| echo "Configs pushed to dash-network-configs repo" | |
| - name: Verify platform services | |
| if: success() | |
| run: | | |
| echo "Verifying platform services on HP masternodes..." | |
| ansible hp_masternodes \ | |
| -i "networks/$NETWORK_NAME.inventory" \ | |
| --private-key="$HOME/.ssh/id_rsa" \ | |
| -b -m shell \ | |
| -a 'sudo -u dashmate dashmate status services --format=json | jq -r ".[] | select(.service != \"core\") | \"\(.service): \(.status)\""' | |
| - name: Print summary | |
| if: always() | |
| run: | | |
| echo "============================================" | |
| echo "Devnet: $NETWORK_NAME" | |
| echo "============================================" | |
| echo "" | |
| echo "To update this devnet later, use the 'Platform Version Deployment' workflow" | |
| echo "To destroy this devnet, use the 'Destroy Devnet' workflow" |