From fd5626cfde671b15546c5ed29d7c9ae5e13b1859 Mon Sep 17 00:00:00 2001 From: ktechmidas <9920871+ktechmidas@users.noreply.github.com> Date: Tue, 24 Feb 2026 14:19:06 +0300 Subject: [PATCH 1/4] feat(ci): add Create and Destroy Devnet GitHub Actions workflows Add two new workflow_dispatch workflows for full devnet lifecycle management: - create-devnet.yml: Generates configs, provisions AWS infrastructure via Terraform, deploys all services via Ansible, and persists configs to the dash-network-configs repo. Defaults to 11 ARM HP masternodes, 0 regular masternodes, 1 seed node. Advanced options (node counts, disk size, core version) have sane defaults but can be overridden. - destroy-devnet.yml: Tears down devnet infrastructure (all/platform/network targets), cleans up configs from dash-network-configs repo when fully destroyed. Required secrets: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, TERRAFORM_S3_BUCKET, TERRAFORM_S3_KEY, TERRAFORM_DYNAMODB_TABLE (plus existing DEPLOY_SERVER_KEY and EVO_APP_DEPLOY_KEY). Co-Authored-By: Claude Opus 4.6 --- .github/workflows/create-devnet.yml | 270 +++++++++++++++++++++++++++ .github/workflows/destroy-devnet.yml | 198 ++++++++++++++++++++ 2 files changed, 468 insertions(+) create mode 100644 .github/workflows/create-devnet.yml create mode 100644 .github/workflows/destroy-devnet.yml diff --git a/.github/workflows/create-devnet.yml b/.github/workflows/create-devnet.yml new file mode 100644 index 00000000..0054b65b --- /dev/null +++ b/.github/workflows/create-devnet.yml @@ -0,0 +1,270 @@ +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" + # 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-latest + timeout-minutes: 120 + + 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 }} + ANSIBLE_HOST_KEY_CHECKING: "false" + + steps: + - name: Validate devnet name + run: | + NAME="${{ github.event.inputs.devnet_name }}" + 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: | + python -m pip install --upgrade pip + pip install ansible + + - 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 + run: | + mkdir -p ~/.ssh + + # Server SSH key for connecting to nodes + echo "${{ secrets.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 + echo "${{ secrets.EVO_APP_DEPLOY_KEY }}" > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + + # 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=${{ 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 }} + EOF + + - name: Generate network configs + run: | + echo "Generating configs for $NETWORK_NAME..." + chmod +x ./bin/generate + ./bin/generate "$NETWORK_NAME" \ + "${{ github.event.inputs.masternodes_amd_count }}" \ + "${{ github.event.inputs.masternodes_arm_count }}" \ + "${{ github.event.inputs.hp_masternodes_amd_count }}" \ + "${{ github.event.inputs.hp_masternodes_arm_count }}" \ + -s="${{ github.event.inputs.seed_count }}" + + echo "Generated config files:" + ls -la networks/devnet-* + + - name: Update platform version in config + run: | + CONFIG_FILE="networks/$NETWORK_NAME.yml" + VERSION="${{ github.event.inputs.platform_version }}" + + echo "Setting platform version to $VERSION..." + + # Update dashmate version + sed -i "s/dashmate_version: .*/dashmate_version: $VERSION/" "$CONFIG_FILE" + + # Update platform service images + sed -i "s|drive_image: dashpay/drive:.*|drive_image: dashpay/drive:$VERSION|" "$CONFIG_FILE" + sed -i "s|dapi_image: dashpay/dapi:.*|dapi_image: dashpay/dapi:$VERSION|" "$CONFIG_FILE" + + # Update core version if specified + CORE_VERSION="${{ github.event.inputs.core_version }}" + if [[ -n "$CORE_VERSION" ]]; then + echo "Setting core version to $CORE_VERSION..." + sed -i "s|dashd_image: dashpay/dashd:.*|dashd_image: dashpay/dashd:$CORE_VERSION|" "$CONFIG_FILE" + fi + + echo "Updated config:" + grep -E "(dashmate_version|drive_image|dapi_image|dashd_image)" "$CONFIG_FILE" + + - name: Update terraform config + run: | + TFVARS_FILE="networks/$NETWORK_NAME.tfvars" + DISK_SIZE="${{ github.event.inputs.hpmn_disk_size }}" + + if [[ "$DISK_SIZE" != "30" ]]; then + echo "Setting HP masternode disk size to ${DISK_SIZE}GB..." + if grep -q "hpmn_node_disk_size" "$TFVARS_FILE"; 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) + run: | + echo "============================================" + echo "Deploying $NETWORK_NAME" + echo "============================================" + + chmod +x ./bin/deploy + ./bin/deploy "$NETWORK_NAME" + + - name: Push configs to dash-network-configs + run: | + DEVNET_NAME="${{ github.event.inputs.devnet_name }}" + + # 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 + cp "networks/$NETWORK_NAME.yml" /tmp/dash-network-configs/ + cp "networks/$NETWORK_NAME.tfvars" /tmp/dash-network-configs/ + cp "networks/$NETWORK_NAME.inventory" /tmp/dash-network-configs/ + + # 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" + git push + + echo "Configs pushed to dash-network-configs repo" + + - name: Verify platform services + if: always() + 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)\""' \ + || true + + - name: Print summary + if: always() + run: | + echo "============================================" + echo "Devnet: $NETWORK_NAME" + echo "Platform Version: ${{ github.event.inputs.platform_version }}" + echo "HP Masternodes: ${{ github.event.inputs.hp_masternodes_amd_count }} AMD + ${{ github.event.inputs.hp_masternodes_arm_count }} ARM" + echo "Regular Masternodes: ${{ github.event.inputs.masternodes_amd_count }} AMD + ${{ github.event.inputs.masternodes_arm_count }} ARM" + echo "Seed Nodes: ${{ github.event.inputs.seed_count }}" + echo "============================================" + echo "" + echo "To update this devnet later, use the 'Platform Version Deployment' workflow" + echo "To destroy this devnet, use the 'Destroy Devnet' workflow" diff --git a/.github/workflows/destroy-devnet.yml b/.github/workflows/destroy-devnet.yml new file mode 100644 index 00000000..761575e3 --- /dev/null +++ b/.github/workflows/destroy-devnet.yml @@ -0,0 +1,198 @@ +name: Destroy Devnet + +on: + workflow_dispatch: + inputs: + devnet_name: + description: "Devnet name (without 'devnet-' prefix, e.g. 'mytest' destroys 'devnet-mytest')" + required: true + type: string + destroy_target: + description: "What to destroy" + required: true + type: choice + default: "all" + options: + - all + - platform + - network + +jobs: + destroy: + name: Destroy Devnet + runs-on: ubuntu-latest + timeout-minutes: 60 + + 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 }} + ANSIBLE_HOST_KEY_CHECKING: "false" + + steps: + - name: Validate devnet name + run: | + NAME="${{ github.event.inputs.devnet_name }}" + 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 + echo "Will destroy: devnet-$NAME (target: ${{ github.event.inputs.destroy_target }})" + + - 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: | + python -m pip install --upgrade pip + pip install ansible + + - 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 + run: | + mkdir -p ~/.ssh + + # Server SSH key for connecting to nodes + echo "${{ secrets.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 + echo "${{ secrets.EVO_APP_DEPLOY_KEY }}" > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + + # 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=${{ 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 }} + EOF + + - name: Clone network configs + run: | + rm -rf networks/.git + git clone git@github.com:dashpay/dash-network-configs.git /tmp/dash-network-configs + + # Copy config files for this devnet + cp /tmp/dash-network-configs/$NETWORK_NAME.yml networks/ 2>/dev/null || true + cp /tmp/dash-network-configs/$NETWORK_NAME.tfvars networks/ 2>/dev/null || true + cp /tmp/dash-network-configs/$NETWORK_NAME.inventory networks/ 2>/dev/null || true + + - name: Validate config files exist + run: | + if [[ ! -f "networks/$NETWORK_NAME.yml" ]]; then + echo "Error: Config file networks/$NETWORK_NAME.yml not found" + echo "Available configs in dash-network-configs:" + ls /tmp/dash-network-configs/*.yml 2>/dev/null || echo " (none)" + exit 1 + fi + echo "Found config files for $NETWORK_NAME" + ls -la networks/$NETWORK_NAME.* + + - name: Print destruction plan + run: | + echo "============================================" + echo "WARNING: Destroying $NETWORK_NAME" + echo "Target: ${{ github.event.inputs.destroy_target }}" + echo "============================================" + echo "" + case "${{ github.event.inputs.destroy_target }}" in + all) + echo "This will DESTROY ALL INFRASTRUCTURE (EC2 instances, VPCs, etc.)" + echo "and remove configs from the dash-network-configs repo." + ;; + platform) + echo "This will destroy platform services on HP masternodes." + echo "Infrastructure will be kept." + ;; + network) + echo "This will destroy all services and configs on nodes." + echo "Infrastructure will be kept." + ;; + esac + echo "" + + - name: Destroy devnet + run: | + chmod +x ./bin/destroy + ./bin/destroy "$NETWORK_NAME" -t="${{ github.event.inputs.destroy_target }}" + + - name: Remove configs from dash-network-configs + if: github.event.inputs.destroy_target == 'all' + run: | + cd /tmp/dash-network-configs + git config user.name "GitHub Actions" + git config user.email "actions@github.com" + + # Remove config files for this devnet + git rm "$NETWORK_NAME.yml" 2>/dev/null || true + git rm "$NETWORK_NAME.tfvars" 2>/dev/null || true + git rm "$NETWORK_NAME.inventory" 2>/dev/null || true + + git commit -m "Remove configs for $NETWORK_NAME (destroyed)" || echo "No changes to commit" + git push + + echo "Configs removed from dash-network-configs repo" + + - name: Print summary + if: always() + run: | + echo "============================================" + echo "Devnet: $NETWORK_NAME" + echo "Target: ${{ github.event.inputs.destroy_target }}" + echo "Status: Destruction complete" + echo "============================================" From 4288bd6643e18b74f5dce0c41e9fbf933524fecf Mon Sep 17 00:00:00 2001 From: ktechmidas <9920871+ktechmidas@users.noreply.github.com> Date: Tue, 24 Feb 2026 14:29:18 +0300 Subject: [PATCH 2/4] fix(ci): add rs_dapi_image version to platform deployments group_vars/all defines rs_dapi_image without a version tag, which causes the dashmate template to use it directly (skipping the regex_replace fallback from dapi_image). This meant rs_dapi would get :latest instead of the specified platform version. Fix by explicitly setting rs_dapi_image in both create-devnet and platform-deploy workflows. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/create-devnet.yml | 10 +++++++++- .github/workflows/platform-deploy.yml | 13 +++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/.github/workflows/create-devnet.yml b/.github/workflows/create-devnet.yml index 0054b65b..b0f62c85 100644 --- a/.github/workflows/create-devnet.yml +++ b/.github/workflows/create-devnet.yml @@ -186,6 +186,14 @@ jobs: sed -i "s|drive_image: dashpay/drive:.*|drive_image: dashpay/drive:$VERSION|" "$CONFIG_FILE" sed -i "s|dapi_image: dashpay/dapi:.*|dapi_image: dashpay/dapi:$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:$VERSION|" "$CONFIG_FILE" + fi + # Update core version if specified CORE_VERSION="${{ github.event.inputs.core_version }}" if [[ -n "$CORE_VERSION" ]]; then @@ -194,7 +202,7 @@ jobs: fi echo "Updated config:" - grep -E "(dashmate_version|drive_image|dapi_image|dashd_image)" "$CONFIG_FILE" + grep -E "(dashmate_version|drive_image|dapi_image|rs_dapi_image|dashd_image)" "$CONFIG_FILE" - name: Update terraform config run: | diff --git a/.github/workflows/platform-deploy.yml b/.github/workflows/platform-deploy.yml index 41e8a822..6ec85126 100644 --- a/.github/workflows/platform-deploy.yml +++ b/.github/workflows/platform-deploy.yml @@ -109,9 +109,18 @@ jobs: # Update platform service image versions sed -i "s/drive_image: dashpay\/drive:[^ ]*/drive_image: dashpay\/drive:${{ github.event.inputs.platform_version }}/" networks/${{ github.event.inputs.network }}.yml sed -i "s/dapi_image: dashpay\/dapi:[^ ]*/dapi_image: dashpay\/dapi:${{ github.event.inputs.platform_version }}/" networks/${{ github.event.inputs.network }}.yml - + + # Update rs_dapi_image (group_vars/all defines it without a tag, so we must + # explicitly set it to get the right version instead of :latest) + CONFIG_FILE="networks/${{ github.event.inputs.network }}.yml" + if grep -q "rs_dapi_image:" "$CONFIG_FILE"; then + sed -i "s|rs_dapi_image: dashpay/rs-dapi:[^ ]*|rs_dapi_image: dashpay/rs-dapi:${{ github.event.inputs.platform_version }}|" "$CONFIG_FILE" + else + echo "rs_dapi_image: dashpay/rs-dapi:${{ github.event.inputs.platform_version }}" >> "$CONFIG_FILE" + fi + echo "Updated network config:" - grep -E "(dashmate_version|drive_image|dapi_image)" networks/${{ github.event.inputs.network }}.yml + grep -E "(dashmate_version|drive_image|dapi_image|rs_dapi_image)" networks/${{ github.event.inputs.network }}.yml # Run platform deployment - name: Run Platform Deployment From f9f8dbbe52aceb974161727c6f40bf60eb62b1a7 Mon Sep 17 00:00:00 2001 From: ktechmidas <9920871+ktechmidas@users.noreply.github.com> Date: Tue, 24 Feb 2026 14:40:04 +0300 Subject: [PATCH 3/4] fix(ci): harden devnet workflows against injection and improve reliability - Map all github.event.inputs and secrets to env vars instead of inline ${{ }} expansion in run blocks to prevent shell injection - Write SSH keys with printf instead of echo for safe handling - Use python3 -m pip instead of ambiguous python binary - Add numeric validation for node count inputs before passing to bin/generate - Escape sed-special characters (& \ /) in version strings - Change verify step from if: always() to if: success() and remove || true so Ansible errors are surfaced - Add concurrency groups to prevent parallel runs racing on Terraform state or git push for the same devnet - Validate all three config files (yml, tfvars, inventory) exist in destroy workflow, not just yml - Compare disk size against actual file value instead of hardcoded default - Reference DESTROY_TARGET from job-level env instead of inline expansion in shell blocks Co-Authored-By: Claude Opus 4.6 --- .github/workflows/create-devnet.yml | 98 ++++++++++++++++++---------- .github/workflows/destroy-devnet.yml | 61 +++++++++++------ 2 files changed, 105 insertions(+), 54 deletions(-) diff --git a/.github/workflows/create-devnet.yml b/.github/workflows/create-devnet.yml index b0f62c85..1225dee1 100644 --- a/.github/workflows/create-devnet.yml +++ b/.github/workflows/create-devnet.yml @@ -54,18 +54,25 @@ jobs: name: Create Devnet runs-on: ubuntu-latest 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: | - NAME="${{ github.event.inputs.devnet_name }}" if [[ -z "$NAME" ]]; then echo "Error: devnet_name is required" exit 1 @@ -104,8 +111,8 @@ jobs: - name: Install Ansible run: | - python -m pip install --upgrade pip - pip install ansible + python3 -m pip install --upgrade pip + python3 -m pip install ansible - name: Install Ansible roles run: | @@ -114,11 +121,14 @@ jobs: 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 }} run: | mkdir -p ~/.ssh # Server SSH key for connecting to nodes - echo "${{ secrets.DEPLOY_SERVER_KEY }}" > ~/.ssh/id_rsa + printf '%s\n' "$DEPLOY_SERVER_KEY" > ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa # Derive public key from private key @@ -126,7 +136,7 @@ jobs: chmod 644 ~/.ssh/id_rsa.pub # GitHub deploy key for cloning configs repo - echo "${{ secrets.EVO_APP_DEPLOY_KEY }}" > ~/.ssh/id_ed25519 + printf '%s\n' "$EVO_APP_DEPLOY_KEY" > ~/.ssh/id_ed25519 chmod 600 ~/.ssh/id_ed25519 # SSH config @@ -150,68 +160,93 @@ jobs: cat > networks/.env << EOF PRIVATE_KEY_PATH=$HOME/.ssh/id_rsa PUBLIC_KEY_PATH=$HOME/.ssh/id_rsa.pub - 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 }} + 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: Generate network configs + 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" \ - "${{ github.event.inputs.masternodes_amd_count }}" \ - "${{ github.event.inputs.masternodes_arm_count }}" \ - "${{ github.event.inputs.hp_masternodes_amd_count }}" \ - "${{ github.event.inputs.hp_masternodes_arm_count }}" \ - -s="${{ github.event.inputs.seed_count }}" + "$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" - VERSION="${{ github.event.inputs.platform_version }}" + + # 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: $VERSION/" "$CONFIG_FILE" + 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:$VERSION|" "$CONFIG_FILE" - sed -i "s|dapi_image: dashpay/dapi:.*|dapi_image: dashpay/dapi:$VERSION|" "$CONFIG_FILE" + 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:$VERSION|" "$CONFIG_FILE" + 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 - CORE_VERSION="${{ github.event.inputs.core_version }}" if [[ -n "$CORE_VERSION" ]]; then echo "Setting core version to $CORE_VERSION..." - sed -i "s|dashd_image: dashpay/dashd:.*|dashd_image: dashpay/dashd:$CORE_VERSION|" "$CONFIG_FILE" + 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" - DISK_SIZE="${{ github.event.inputs.hpmn_disk_size }}" - if [[ "$DISK_SIZE" != "30" ]]; then + # 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 "") + + 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 grep -q "hpmn_node_disk_size" "$TFVARS_FILE"; then + 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" @@ -232,8 +267,6 @@ jobs: - name: Push configs to dash-network-configs run: | - DEVNET_NAME="${{ github.event.inputs.devnet_name }}" - # Clone the configs repo to a temp directory git clone git@github.com:dashpay/dash-network-configs.git /tmp/dash-network-configs @@ -253,25 +286,20 @@ jobs: echo "Configs pushed to dash-network-configs repo" - name: Verify platform services - if: always() + 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)\""' \ - || true + -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 "Platform Version: ${{ github.event.inputs.platform_version }}" - echo "HP Masternodes: ${{ github.event.inputs.hp_masternodes_amd_count }} AMD + ${{ github.event.inputs.hp_masternodes_arm_count }} ARM" - echo "Regular Masternodes: ${{ github.event.inputs.masternodes_amd_count }} AMD + ${{ github.event.inputs.masternodes_arm_count }} ARM" - echo "Seed Nodes: ${{ github.event.inputs.seed_count }}" echo "============================================" echo "" echo "To update this devnet later, use the 'Platform Version Deployment' workflow" diff --git a/.github/workflows/destroy-devnet.yml b/.github/workflows/destroy-devnet.yml index 761575e3..a359bafa 100644 --- a/.github/workflows/destroy-devnet.yml +++ b/.github/workflows/destroy-devnet.yml @@ -22,18 +22,26 @@ jobs: name: Destroy Devnet runs-on: ubuntu-latest timeout-minutes: 60 + concurrency: + group: "devnet-${{ github.event.inputs.devnet_name }}" + cancel-in-progress: false env: NETWORK_NAME: "devnet-${{ github.event.inputs.devnet_name }}" + DESTROY_TARGET: ${{ github.event.inputs.destroy_target }} 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: | - NAME="${{ github.event.inputs.devnet_name }}" if [[ -z "$NAME" ]]; then echo "Error: devnet_name is required" exit 1 @@ -42,7 +50,7 @@ jobs: echo "Error: Do not include 'devnet-' prefix. Just provide the name (e.g. 'mytest')" exit 1 fi - echo "Will destroy: devnet-$NAME (target: ${{ github.event.inputs.destroy_target }})" + echo "Will destroy: devnet-$NAME (target: $DESTROY_TARGET)" - name: Checkout dash-network-deploy uses: actions/checkout@v4 @@ -68,8 +76,8 @@ jobs: - name: Install Ansible run: | - python -m pip install --upgrade pip - pip install ansible + python3 -m pip install --upgrade pip + python3 -m pip install ansible - name: Install Ansible roles run: | @@ -78,11 +86,14 @@ jobs: 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 }} run: | mkdir -p ~/.ssh # Server SSH key for connecting to nodes - echo "${{ secrets.DEPLOY_SERVER_KEY }}" > ~/.ssh/id_rsa + printf '%s\n' "$DEPLOY_SERVER_KEY" > ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa # Derive public key from private key @@ -90,7 +101,7 @@ jobs: chmod 644 ~/.ssh/id_rsa.pub # GitHub deploy key for cloning configs repo - echo "${{ secrets.EVO_APP_DEPLOY_KEY }}" > ~/.ssh/id_ed25519 + printf '%s\n' "$EVO_APP_DEPLOY_KEY" > ~/.ssh/id_ed25519 chmod 600 ~/.ssh/id_ed25519 # SSH config @@ -114,12 +125,12 @@ jobs: cat > networks/.env << EOF PRIVATE_KEY_PATH=$HOME/.ssh/id_rsa PUBLIC_KEY_PATH=$HOME/.ssh/id_rsa.pub - 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 }} + 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: Clone network configs @@ -134,23 +145,35 @@ jobs: - name: Validate config files exist run: | - if [[ ! -f "networks/$NETWORK_NAME.yml" ]]; then - echo "Error: Config file networks/$NETWORK_NAME.yml not found" + MISSING=() + for ext in yml tfvars inventory; do + if [[ ! -f "networks/$NETWORK_NAME.$ext" ]]; then + MISSING+=("networks/$NETWORK_NAME.$ext") + fi + done + + if [[ ${#MISSING[@]} -gt 0 ]]; then + echo "Error: Missing config file(s):" + for f in "${MISSING[@]}"; do + echo " - $f" + done + echo "" echo "Available configs in dash-network-configs:" ls /tmp/dash-network-configs/*.yml 2>/dev/null || echo " (none)" exit 1 fi - echo "Found config files for $NETWORK_NAME" + + echo "Found all config files for $NETWORK_NAME" ls -la networks/$NETWORK_NAME.* - name: Print destruction plan run: | echo "============================================" echo "WARNING: Destroying $NETWORK_NAME" - echo "Target: ${{ github.event.inputs.destroy_target }}" + echo "Target: $DESTROY_TARGET" echo "============================================" echo "" - case "${{ github.event.inputs.destroy_target }}" in + case "$DESTROY_TARGET" in all) echo "This will DESTROY ALL INFRASTRUCTURE (EC2 instances, VPCs, etc.)" echo "and remove configs from the dash-network-configs repo." @@ -169,7 +192,7 @@ jobs: - name: Destroy devnet run: | chmod +x ./bin/destroy - ./bin/destroy "$NETWORK_NAME" -t="${{ github.event.inputs.destroy_target }}" + ./bin/destroy "$NETWORK_NAME" -t="$DESTROY_TARGET" - name: Remove configs from dash-network-configs if: github.event.inputs.destroy_target == 'all' @@ -193,6 +216,6 @@ jobs: run: | echo "============================================" echo "Devnet: $NETWORK_NAME" - echo "Target: ${{ github.event.inputs.destroy_target }}" + echo "Target: $DESTROY_TARGET" echo "Status: Destruction complete" echo "============================================" From 681fdd56d273008ad2fa6748357f90f19843df52 Mon Sep 17 00:00:00 2001 From: vivekgsharma Date: Tue, 3 Mar 2026 13:09:48 +0000 Subject: [PATCH 4/4] Fix create-devnet workflow deploy step for GitHub Actions detached HEAD --- .github/workflows/create-devnet.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/create-devnet.yml b/.github/workflows/create-devnet.yml index 1225dee1..c1475573 100644 --- a/.github/workflows/create-devnet.yml +++ b/.github/workflows/create-devnet.yml @@ -263,7 +263,8 @@ jobs: echo "============================================" chmod +x ./bin/deploy - ./bin/deploy "$NETWORK_NAME" + # GitHub Actions checks out a detached HEAD; bypass branch safety check. + ./bin/deploy -f "$NETWORK_NAME" - name: Push configs to dash-network-configs run: |