diff --git a/.azure/app-insights.bicep b/.azure/app-insights.bicep index 6e7de6df..d7c0e05f 100644 --- a/.azure/app-insights.bicep +++ b/.azure/app-insights.bicep @@ -1,17 +1,28 @@ @description('Location for Application Insights') param location string = resourceGroup().location +@description('Environment name (dev or prod)') +@allowed([ + 'dev' + 'prod' +]) +param environmentName string + @description('Tags to apply to resources') param tags object = {} var appInsightsName = 'yolo-funk-insights' var logAnalyticsName = 'yolo-funk-logs' +var resourceTags = union(tags, { + Environment: environmentName + ManagedBy: 'GitHub' +}) // Log Analytics Workspace (required for Application Insights) resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { name: logAnalyticsName location: location - tags: tags + tags: resourceTags properties: { sku: { name: 'PerGB2018' @@ -20,11 +31,10 @@ resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { } } -// Application Insights (shared across all environments) resource appInsights 'Microsoft.Insights/components@2020-02-02' = { name: appInsightsName location: location - tags: tags + tags: resourceTags kind: 'web' properties: { Application_Type: 'web' diff --git a/.azure/function-app.bicep b/.azure/function-app.bicep index 2192116f..20980fb7 100644 --- a/.azure/function-app.bicep +++ b/.azure/function-app.bicep @@ -1,4 +1,8 @@ -@description('Environment name (dev, prod, or pr-{number})') +@description('Environment name (dev or prod)') +@allowed([ + 'dev' + 'prod' +]) param environmentName string @description('Location for all resources') @@ -11,8 +15,8 @@ param location string = resourceGroup().location ]) param hyperliquidNetwork string = 'testnet' -@description('Key Vault name for secrets (optional)') -param keyVaultName string = '' +@description('Name of the Key Vault dedicated to this environment') +param keyVaultName string @description('Tags to apply to all resources') param tags object = {} @@ -20,17 +24,11 @@ param tags object = {} var functionAppName = 'yolo-funk-${environmentName}' var storageAccountName = 'yolofunk${uniqueString(resourceGroup().id, environmentName)}' var appInsightsName = 'yolo-funk-insights' -var contentShareName = contains([ - 'dev' - 'prod' -], environmentName) ? toLower(functionAppName) : 'yolofunk${uniqueString(resourceGroup().id, environmentName)}' - -// Determine secret suffix based on network (testnet or mainnet) -var secretEnv = hyperliquidNetwork == 'mainnet' ? 'prod' : 'dev' +var contentShareName = toLower(functionAppName) var useTestnet = hyperliquidNetwork == 'mainnet' ? 'false' : 'true' // Key Vault reference helper -var keyVaultUri = !empty(keyVaultName) ? 'https://${keyVaultName}${environment().suffixes.keyvaultDns}' : '' +var keyVaultUri = 'https://${keyVaultName}${environment().suffixes.keyvaultDns}' // Merge environment tags with provided tags var resourceTags = union(tags, { @@ -54,7 +52,7 @@ resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = { } } -// Application Insights (shared across environments for cost savings) +// Each environment has an identically named instance in its own resource group. resource appInsights 'Microsoft.Insights/components@2020-02-02' existing = { name: appInsightsName } @@ -122,21 +120,15 @@ resource functionApp 'Microsoft.Web/sites@2023-01-01' = { } { name: 'Strategies__YoloDaily__Hyperliquid__Address' - value: !empty(keyVaultName) - ? '@Microsoft.KeyVault(SecretUri=${keyVaultUri}/secrets/hyperliquid-${secretEnv}-agent-address/)' - : '' + value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}/secrets/hyperliquid-agent-address/)' } { name: 'Strategies__YoloDaily__Hyperliquid__PrivateKey' - value: !empty(keyVaultName) - ? '@Microsoft.KeyVault(SecretUri=${keyVaultUri}/secrets/hyperliquid-${secretEnv}-agent-privatekey/)' - : '' + value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}/secrets/hyperliquid-agent-privatekey/)' } { name: 'Strategies__YoloDaily__Hyperliquid__VaultAddress' - value: !empty(keyVaultName) - ? '@Microsoft.KeyVault(SecretUri=${keyVaultUri}/secrets/hyperliquid-${secretEnv}-vault-yolodaily/)' - : '' + value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}/secrets/hyperliquid-vault-yolodaily/)' } { name: 'Strategies__YoloDaily__Hyperliquid__UseTestnet' @@ -144,21 +136,15 @@ resource functionApp 'Microsoft.Web/sites@2023-01-01' = { } { name: 'Strategies__UnravelDaily__Hyperliquid__Address' - value: !empty(keyVaultName) - ? '@Microsoft.KeyVault(SecretUri=${keyVaultUri}/secrets/hyperliquid-${secretEnv}-agent-address/)' - : '' + value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}/secrets/hyperliquid-agent-address/)' } { name: 'Strategies__UnravelDaily__Hyperliquid__PrivateKey' - value: !empty(keyVaultName) - ? '@Microsoft.KeyVault(SecretUri=${keyVaultUri}/secrets/hyperliquid-${secretEnv}-agent-privatekey/)' - : '' + value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}/secrets/hyperliquid-agent-privatekey/)' } { name: 'Strategies__UnravelDaily__Hyperliquid__VaultAddress' - value: !empty(keyVaultName) - ? '@Microsoft.KeyVault(SecretUri=${keyVaultUri}/secrets/hyperliquid-${secretEnv}-vault-unraveldaily/)' - : '' + value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}/secrets/hyperliquid-vault-unraveldaily/)' } { name: 'Strategies__UnravelDaily__Hyperliquid__UseTestnet' @@ -166,13 +152,11 @@ resource functionApp 'Microsoft.Web/sites@2023-01-01' = { } { name: 'Strategies__YoloDaily__RobotWealth__ApiKey' - value: !empty(keyVaultName) - ? '@Microsoft.KeyVault(SecretUri=${keyVaultUri}/secrets/robotwealth-api-key/)' - : '' + value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}/secrets/robotwealth-api-key/)' } { name: 'Strategies__UnravelDaily__Unravel__ApiKey' - value: !empty(keyVaultName) ? '@Microsoft.KeyVault(SecretUri=${keyVaultUri}/secrets/unravel-api-key/)' : '' + value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}/secrets/unravel-api-key/)' } { name: 'Strategies__YoloDaily__Schedule' diff --git a/.azure/key-vault.bicep b/.azure/key-vault.bicep new file mode 100644 index 00000000..4c7000d6 --- /dev/null +++ b/.azure/key-vault.bicep @@ -0,0 +1,48 @@ +@description('Globally unique Key Vault name for this environment') +param keyVaultName string + +@description('Environment name (dev or prod)') +@allowed([ + 'dev' + 'prod' +]) +param environmentName string + +@description('Location for the Key Vault') +param location string = resourceGroup().location + +@description('Tags to apply to the Key Vault') +param tags object = {} + +resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = { + name: keyVaultName + location: location + tags: union(tags, { + Environment: environmentName + ManagedBy: 'GitHub' + }) + properties: { + tenantId: subscription().tenantId + enableRbacAuthorization: true + enableSoftDelete: true + softDeleteRetentionInDays: 90 + enablePurgeProtection: true + // GitHub-hosted runners and operator workstations do not have stable egress IPs. + // Keep the data-plane endpoint public; Entra authentication and vault-scoped RBAC + // remain mandatory for both deployments and manual secret maintenance. + publicNetworkAccess: 'Enabled' + networkAcls: { + bypass: 'None' + defaultAction: 'Allow' + ipRules: [] + virtualNetworkRules: [] + } + sku: { + family: 'A' + name: 'standard' + } + } +} + +output keyVaultId string = keyVault.id +output keyVaultUri string = keyVault.properties.vaultUri diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8ca517d6..d2c1cc24 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,5 +7,6 @@ version: 2 updates: - package-ecosystem: "nuget" # See documentation for possible values directory: "/" # Location of package manifests + target-branch: "develop" schedule: interval: "weekly" diff --git a/.github/workflows/cleanup-azure-functions.yml b/.github/workflows/cleanup-azure-functions.yml deleted file mode 100644 index a2674be7..00000000 --- a/.github/workflows/cleanup-azure-functions.yml +++ /dev/null @@ -1,182 +0,0 @@ -name: Cleanup Azure Functions - -permissions: {} - -on: - pull_request: - types: [closed] - workflow_dispatch: - inputs: - environment: - description: "Environment to cleanup (pr-{number}, feat-{name}, or dev)" - required: true - type: string - principal_id: - description: "Optional managed identity principal/object id to remove from Key Vault if the app was already deleted" - required: false - type: string - -env: - AZURE_RESOURCE_GROUP: ${{ vars.AZURE_RESOURCE_GROUP || 'ResourceGroup1' }} - AZURE_KEYVAULT_NAME: ${{ vars.AZURE_KEYVAULT_NAME || 'YOLO' }} - -jobs: - cleanup: - permissions: - pull-requests: write - runs-on: ubuntu-latest - steps: - - name: Determine environment to cleanup - id: set-env - run: | - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - ENV="${{ github.event.inputs.environment }}" - PRINCIPAL_ID="${{ github.event.inputs.principal_id }}" - elif [ "${{ github.event_name }}" = "pull_request" ]; then - ENV="pr-${{ github.event.pull_request.number }}" - PRINCIPAL_ID="" - else - echo "Unknown event type" - exit 1 - fi - - # Never allow cleanup of production - if [ "$ENV" = "prod" ]; then - echo "โŒ Cannot cleanup production environment via automation" - exit 1 - fi - - echo "environment=$ENV" >> $GITHUB_OUTPUT - echo "function-app-name=yolo-funk-$ENV" >> $GITHUB_OUTPUT - echo "principal_id=$PRINCIPAL_ID" >> $GITHUB_OUTPUT - - - name: Azure Login - uses: azure/login@v2 - with: - creds: ${{ secrets.AZURE_CREDENTIALS }} - - - name: Delete Function App and related resources - run: | - FUNCTION_APP_NAME="${{ steps.set-env.outputs.function-app-name }}" - ENV="${{ steps.set-env.outputs.environment }}" - INPUT_PRINCIPAL_ID="${{ steps.set-env.outputs.principal_id }}" - - remove_keyvault_assignment() { - local principal_id="$1" - - if [ -z "$principal_id" ] || [ "$principal_id" = "null" ]; then - echo "No managed identity principal id available for Key Vault role cleanup" - return 0 - fi - - if [ -z "${{ env.AZURE_KEYVAULT_NAME }}" ]; then - echo "No Key Vault configured, skipping role assignment cleanup" - return 0 - fi - - KEYVAULT_ID=$(az keyvault show \ - --name "${{ env.AZURE_KEYVAULT_NAME }}" \ - --query id \ - --output tsv 2>/dev/null || true) - - if [ -z "$KEYVAULT_ID" ]; then - echo "Key Vault ${{ env.AZURE_KEYVAULT_NAME }} not found, skipping role assignment cleanup" - return 0 - fi - - ASSIGNMENT_IDS=$(az role assignment list \ - --scope "$KEYVAULT_ID" \ - --role "Key Vault Secrets User" \ - --query "[?principalId=='$principal_id'].id" \ - --output tsv) - - if [ -z "$ASSIGNMENT_IDS" ]; then - echo "No Key Vault Secrets User assignment found for principal $principal_id" - return 0 - fi - - for assignment_id in $ASSIGNMENT_IDS; do - echo "Deleting Key Vault role assignment: $assignment_id" - az role assignment delete --ids "$assignment_id" - done - } - - # Check if Function App exists - if az functionapp show \ - --resource-group ${{ env.AZURE_RESOURCE_GROUP }} \ - --name $FUNCTION_APP_NAME 2>/dev/null; then - - echo "Deleting Function App: $FUNCTION_APP_NAME" - - # Get associated resources (App Service Plan name from resource ID) - PLAN_ID=$(az functionapp show \ - --resource-group ${{ env.AZURE_RESOURCE_GROUP }} \ - --name $FUNCTION_APP_NAME \ - --query 'appServicePlanId' \ - --output tsv) - - PRINCIPAL_ID=$(az functionapp show \ - --resource-group ${{ env.AZURE_RESOURCE_GROUP }} \ - --name $FUNCTION_APP_NAME \ - --query 'identity.principalId' \ - --output tsv) - - # Extract plan name from resource ID (format: /subscriptions/.../serverfarms/PLAN_NAME) - if [ -n "$PLAN_ID" ]; then - PLAN_NAME=$(echo "$PLAN_ID" | awk -F'/' '{print $NF}') - else - PLAN_NAME="${FUNCTION_APP_NAME}-plan" - fi - - echo "App Service Plan: $PLAN_NAME" - - # Delete Key Vault permission before deleting the system-assigned identity. - remove_keyvault_assignment "$PRINCIPAL_ID" - - # Delete Function App - az functionapp delete \ - --resource-group ${{ env.AZURE_RESOURCE_GROUP }} \ - --name $FUNCTION_APP_NAME - - # Delete App Service Plan - az appservice plan delete \ - --resource-group ${{ env.AZURE_RESOURCE_GROUP }} \ - --name $PLAN_NAME \ - --yes - - # Delete Storage Account using tags for precise identification - STORAGE_ACCOUNTS=$(az storage account list \ - --resource-group ${{ env.AZURE_RESOURCE_GROUP }} \ - --query "[?tags.Environment=='$ENV' && tags.FunctionApp=='$FUNCTION_APP_NAME'].name" \ - --output tsv) - - if [ -n "$STORAGE_ACCOUNTS" ]; then - for STORAGE in $STORAGE_ACCOUNTS; do - echo "Deleting storage account: $STORAGE (Environment=$ENV, FunctionApp=$FUNCTION_APP_NAME)" - az storage account delete \ - --resource-group ${{ env.AZURE_RESOURCE_GROUP }} \ - --name $STORAGE \ - --yes - done - else - echo "No storage accounts found with Environment=$ENV and FunctionApp=$FUNCTION_APP_NAME tags" - fi - - echo "โœ… Cleanup complete for $FUNCTION_APP_NAME" - else - echo "Function App $FUNCTION_APP_NAME not found, skipping cleanup" - remove_keyvault_assignment "$INPUT_PRINCIPAL_ID" - fi - - - name: Comment on PR - if: github.event_name == 'pull_request' - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: `### ๐Ÿงน Environment Cleaned Up\n\nThe ephemeral Azure Functions environment \`${{ steps.set-env.outputs.function-app-name }}\` has been deleted.\n\nAll associated resources (Function App, App Service Plan, Storage Account) have been removed.` - }) diff --git a/.github/workflows/deploy-azure-functions.yml b/.github/workflows/deploy-azure-functions.yml index 1ebc3d26..035f640e 100644 --- a/.github/workflows/deploy-azure-functions.yml +++ b/.github/workflows/deploy-azure-functions.yml @@ -8,24 +8,10 @@ on: branches: - master - develop - paths: - - "Directory.*.props" - - "Yolo.slnx" - - "src/YoloFunk/**" - - ".azure/**" - - ".github/workflows/deploy-azure-functions.yml" - pull_request: - types: [opened, synchronize, reopened] - paths: - - "Directory.*.props" - - "Yolo.slnx" - - "src/YoloFunk/**" - - ".azure/**" - - ".github/workflows/deploy-azure-functions.yml" workflow_dispatch: inputs: environment: - description: "Environment to deploy to" + description: Environment to redeploy required: true type: choice options: @@ -34,342 +20,281 @@ on: env: DOTNET_VERSION: "10.0.x" - FUNCTION_PROJECT: "src/YoloFunk/YoloFunk.csproj" - AZURE_KEYVAULT_NAME: ${{ vars.AZURE_KEYVAULT_NAME || 'YOLO' }} + FUNCTION_PROJECT: src/YoloFunk/YoloFunk.csproj jobs: - determine-environment: + select-environment: runs-on: ubuntu-latest outputs: - environment: ${{ steps.set-env.outputs.environment }} - function-app-name: ${{ steps.set-env.outputs.function-app-name }} - hyperliquid-network: ${{ steps.set-env.outputs.hyperliquid-network }} - deploy: ${{ steps.set-env.outputs.deploy }} + environment: ${{ steps.select.outputs.environment }} + github-environment: ${{ steps.select.outputs.github-environment }} + hyperliquid-network: ${{ steps.select.outputs.hyperliquid-network }} steps: - - name: Determine environment - id: set-env + - name: Select and validate environment + id: select + shell: bash + env: + EVENT_NAME: ${{ github.event_name }} + REQUESTED_ENVIRONMENT: ${{ inputs.environment }} + GIT_REF: ${{ github.ref }} run: | - # Manual dispatch takes precedence - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - ENV="${{ github.event.inputs.environment }}" - echo "environment=$ENV" >> $GITHUB_OUTPUT - echo "function-app-name=yolo-funk-$ENV" >> $GITHUB_OUTPUT - if [ "$ENV" = "prod" ]; then - echo "hyperliquid-network=mainnet" >> $GITHUB_OUTPUT - else - echo "hyperliquid-network=testnet" >> $GITHUB_OUTPUT - fi - echo "deploy=true" >> $GITHUB_OUTPUT - exit 0 + if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then + ENVIRONMENT="$REQUESTED_ENVIRONMENT" + elif [[ "$GIT_REF" == "refs/heads/master" ]]; then + ENVIRONMENT="prod" + elif [[ "$GIT_REF" == "refs/heads/develop" ]]; then + ENVIRONMENT="dev" + else + echo "Unsupported deployment ref: $GIT_REF" + exit 1 fi - # Pull requests create ephemeral environments - if [ "${{ github.event_name }}" = "pull_request" ]; then - # Skip deployment for Dependabot PRs (they don't have access to secrets) - if [ "${{ github.actor }}" = "dependabot[bot]" ]; then - echo "Skipping deployment for Dependabot PR" - echo "deploy=false" >> $GITHUB_OUTPUT - exit 0 - fi - - ENV="pr-${{ github.event.pull_request.number }}" - echo "environment=$ENV" >> $GITHUB_OUTPUT - echo "function-app-name=yolo-funk-$ENV" >> $GITHUB_OUTPUT - echo "hyperliquid-network=testnet" >> $GITHUB_OUTPUT - echo "deploy=true" >> $GITHUB_OUTPUT - exit 0 + if [[ "$ENVIRONMENT" == "prod" ]]; then + EXPECTED_REF="refs/heads/master" + GITHUB_ENVIRONMENT="production" + NETWORK="mainnet" + else + EXPECTED_REF="refs/heads/develop" + GITHUB_ENVIRONMENT="development" + NETWORK="testnet" fi - # Branch-based deployment - if [ "${{ github.ref }}" = "refs/heads/master" ]; then - echo "environment=prod" >> $GITHUB_OUTPUT - echo "function-app-name=yolo-funk-prod" >> $GITHUB_OUTPUT - echo "hyperliquid-network=mainnet" >> $GITHUB_OUTPUT - echo "deploy=true" >> $GITHUB_OUTPUT - elif [ "${{ github.ref }}" = "refs/heads/develop" ]; then - echo "environment=dev" >> $GITHUB_OUTPUT - echo "function-app-name=yolo-funk-dev" >> $GITHUB_OUTPUT - echo "hyperliquid-network=testnet" >> $GITHUB_OUTPUT - echo "deploy=true" >> $GITHUB_OUTPUT - elif [[ "${{ github.ref }}" == refs/heads/feat* ]]; then - # Extract branch name and sanitize for Azure naming - # Handles both feat/* and feature/* patterns - BRANCH_NAME=$(echo "${{ github.ref }}" | sed 's/refs\/heads\/feat[^\/]*\///' | sed 's/[^a-zA-Z0-9-]/-/g' | sed 's/--*/-/g' | sed 's/^-//' | cut -c1-20 | sed 's/-$//') - ENV="feat-$BRANCH_NAME" - echo "environment=$ENV" >> $GITHUB_OUTPUT - echo "function-app-name=yolo-funk-$ENV" >> $GITHUB_OUTPUT - echo "hyperliquid-network=testnet" >> $GITHUB_OUTPUT - echo "deploy=true" >> $GITHUB_OUTPUT - else - echo "deploy=false" >> $GITHUB_OUTPUT + if [[ "$GIT_REF" != "$EXPECTED_REF" ]]; then + echo "Ref $GIT_REF cannot deploy environment $ENVIRONMENT; select $EXPECTED_REF." + exit 1 fi - provision-infrastructure: - needs: determine-environment - if: needs.determine-environment.outputs.deploy == 'true' + echo "environment=$ENVIRONMENT" >> "$GITHUB_OUTPUT" + echo "github-environment=$GITHUB_ENVIRONMENT" >> "$GITHUB_OUTPUT" + echo "hyperliquid-network=$NETWORK" >> "$GITHUB_OUTPUT" + + deploy: + needs: select-environment runs-on: ubuntu-latest - environment: ${{ needs.determine-environment.outputs.environment == 'prod' && 'production' || 'development' }} - outputs: - function-app-name: ${{ steps.deploy-infra.outputs.functionAppName }} + environment: ${{ needs.select-environment.outputs.github-environment }} + env: + DEPLOYMENT_ENVIRONMENT: ${{ needs.select-environment.outputs.environment }} + HYPERLIQUID_NETWORK: ${{ needs.select-environment.outputs.hyperliquid-network }} + DEPLOYMENT_SHA: ${{ github.sha }} + PUBLISH_DIR: ${{ github.workspace }}/publish + AZURE_RESOURCE_GROUP: ${{ vars.AZURE_RESOURCE_GROUP }} + AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} + AZURE_KEYVAULT_NAME: ${{ vars.AZURE_KEYVAULT_NAME }} + ALERT_EMAIL: ${{ vars.ALERT_EMAIL }} steps: - - name: Checkout code + - name: Checkout exact deployment commit uses: actions/checkout@v4 + with: + ref: ${{ github.sha }} + fetch-depth: 0 - - name: Azure Login + - name: Verify deployment commit + shell: bash + run: | + CHECKED_OUT_SHA=$(git rev-parse HEAD) + if [[ "$CHECKED_OUT_SHA" != "$DEPLOYMENT_SHA" ]]; then + echo "Checked out $CHECKED_OUT_SHA, expected $DEPLOYMENT_SHA" + exit 1 + fi + echo "Deploying $CHECKED_OUT_SHA to $DEPLOYMENT_ENVIRONMENT" + + - name: Verify production promotion source + if: env.DEPLOYMENT_ENVIRONMENT == 'prod' + shell: bash + run: | + if ! SECOND_PARENT=$(git rev-parse "$DEPLOYMENT_SHA^2" 2>/dev/null); then + echo "Production commits must be merge commits from a develop release PR." + exit 1 + fi + + git fetch origin develop + if ! git merge-base --is-ancestor "$SECOND_PARENT" origin/develop; then + echo "The production merge commit's source is not part of develop." + exit 1 + fi + + - name: Validate environment configuration + shell: bash + run: | + for variable in AZURE_RESOURCE_GROUP AZURE_LOCATION AZURE_KEYVAULT_NAME; do + if [[ -z "${!variable}" ]]; then + echo "Missing GitHub environment variable: $variable" + exit 1 + fi + done + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Restore + run: dotnet restore ${{ env.FUNCTION_PROJECT }} + + - name: Build + run: dotnet build ${{ env.FUNCTION_PROJECT }} --configuration Release --no-restore + + - name: Publish + run: dotnet publish ${{ env.FUNCTION_PROJECT }} --configuration Release --no-build --output ${{ env.PUBLISH_DIR }} + + - name: Verify publish output + shell: bash + run: | + test -f "$PUBLISH_DIR/host.json" + test -f "$PUBLISH_DIR/functions.metadata" + test -f "$PUBLISH_DIR/worker.config.json" + + - name: Azure login uses: azure/login@v2 with: creds: ${{ secrets.AZURE_CREDENTIALS }} - - name: Verify GitHub Service Principal Can Manage Key Vault Role Assignments - if: env.AZURE_KEYVAULT_NAME != '' + - name: Ensure resource group exists run: | - ROLE_ASSIGNMENT_ADMIN_ROLE="User Access Administrator" + az group create \ + --name "$AZURE_RESOURCE_GROUP" \ + --location "$AZURE_LOCATION" \ + --tags Environment="$DEPLOYMENT_ENVIRONMENT" ManagedBy=GitHub - # Get the service principal's object ID from the logged-in account. - SP_OBJECT_ID=$(az ad sp show --id $(az account show --query user.name -o tsv) --query id -o tsv) + - name: Deploy environment Key Vault + run: | + az deployment group create \ + --resource-group "$AZURE_RESOURCE_GROUP" \ + --template-file .azure/key-vault.bicep \ + --parameters \ + keyVaultName="$AZURE_KEYVAULT_NAME" \ + environmentName="$DEPLOYMENT_ENVIRONMENT" \ + location="$AZURE_LOCATION" + - name: Verify Key Vault role-assignment permissions + shell: bash + run: | KEYVAULT_ID=$(az keyvault show \ - --name "${{ env.AZURE_KEYVAULT_NAME }}" \ - --query id \ - --output tsv) + --name "$AZURE_KEYVAULT_NAME" \ + --resource-group "$AZURE_RESOURCE_GROUP" \ + --query id --output tsv) - ROLE_ASSIGNMENT_ADMIN=$(az role assignment list \ + SP_CLIENT_ID=$(az account show --query user.name --output tsv) + SP_OBJECT_ID=$(az ad sp show --id "$SP_CLIENT_ID" --query id --output tsv) + ROLE=$(az role assignment list \ --assignee "$SP_OBJECT_ID" \ --scope "$KEYVAULT_ID" \ --include-inherited \ - --query "[?roleDefinitionName=='$ROLE_ASSIGNMENT_ADMIN_ROLE' || roleDefinitionName=='Owner'].id | [0]" \ + --query "[?roleDefinitionName=='User Access Administrator' || roleDefinitionName=='Owner'].id | [0]" \ --output tsv) - if [ -z "$ROLE_ASSIGNMENT_ADMIN" ]; then - echo "โŒ The GitHub Actions Azure service principal ($SP_OBJECT_ID) cannot manage role assignments on Key Vault ${{ env.AZURE_KEYVAULT_NAME }}." - echo "Grant it '$ROLE_ASSIGNMENT_ADMIN_ROLE' at Key Vault scope, or Owner at an inherited scope, then rerun this workflow." - echo "" - echo "Example:" - echo "az role assignment create --assignee-object-id $SP_OBJECT_ID --assignee-principal-type ServicePrincipal --role '$ROLE_ASSIGNMENT_ADMIN_ROLE' --scope '$KEYVAULT_ID'" + if [[ -z "$ROLE" ]]; then + echo "The deployment identity cannot manage role assignments on this environment's Key Vault." exit 1 fi - echo "โœ… GitHub Actions service principal can manage Key Vault role assignments" - - - name: Ensure Resource Group exists + - name: Deploy environment monitoring run: | - az group create \ - --name ${{ vars.AZURE_RESOURCE_GROUP }} \ - --location ${{ vars.AZURE_LOCATION }} \ - --tags Environment=${{ needs.determine-environment.outputs.environment }} ManagedBy=GitHub - - - name: Deploy shared Application Insights (if not exists) - run: | - if ! az monitor app-insights component show \ - --resource-group ${{ vars.AZURE_RESOURCE_GROUP }} \ - --app yolo-funk-insights 2>/dev/null; then - az deployment group create \ - --resource-group ${{ vars.AZURE_RESOURCE_GROUP }} \ - --template-file .azure/app-insights.bicep \ - --parameters location=${{ vars.AZURE_LOCATION }} - fi + az deployment group create \ + --resource-group "$AZURE_RESOURCE_GROUP" \ + --template-file .azure/app-insights.bicep \ + --parameters location="$AZURE_LOCATION" environmentName="$DEPLOYMENT_ENVIRONMENT" - - name: Deploy Function App Infrastructure - id: deploy-infra + - name: Deploy Function App infrastructure + id: infrastructure + shell: bash run: | - # Set Key Vault name if configured - KEYVAULT_PARAM="" - if [ -n "${{ env.AZURE_KEYVAULT_NAME }}" ]; then - KEYVAULT_PARAM="keyVaultName=${{ env.AZURE_KEYVAULT_NAME }}" - fi - OUTPUT=$(az deployment group create \ - --resource-group ${{ vars.AZURE_RESOURCE_GROUP }} \ + --resource-group "$AZURE_RESOURCE_GROUP" \ --template-file .azure/function-app.bicep \ --parameters \ - environmentName=${{ needs.determine-environment.outputs.environment }} \ - location=${{ vars.AZURE_LOCATION }} \ - hyperliquidNetwork=${{ needs.determine-environment.outputs.hyperliquid-network }} \ - $KEYVAULT_PARAM \ - --query 'properties.outputs' \ + environmentName="$DEPLOYMENT_ENVIRONMENT" \ + location="$AZURE_LOCATION" \ + hyperliquidNetwork="$HYPERLIQUID_NETWORK" \ + keyVaultName="$AZURE_KEYVAULT_NAME" \ + --query properties.outputs \ --output json) - FUNCTION_APP_NAME=$(echo $OUTPUT | jq -r '.functionAppName.value') - PRINCIPAL_ID=$(echo $OUTPUT | jq -r '.principalId.value') + echo "function-app-name=$(jq -r '.functionAppName.value' <<< "$OUTPUT")" >> "$GITHUB_OUTPUT" + echo "principal-id=$(jq -r '.principalId.value' <<< "$OUTPUT")" >> "$GITHUB_OUTPUT" - echo "functionAppName=$FUNCTION_APP_NAME" >> $GITHUB_OUTPUT - echo "principalId=$PRINCIPAL_ID" >> $GITHUB_OUTPUT - echo "Deployed: $FUNCTION_APP_NAME" - - - name: Grant Key Vault Access - if: env.AZURE_KEYVAULT_NAME != '' + - name: Grant Function App access to its Key Vault + shell: bash + env: + PRINCIPAL_ID: ${{ steps.infrastructure.outputs.principal-id }} run: | - PRINCIPAL_ID="${{ steps.deploy-infra.outputs.principalId }}" - ROLE_NAME="Key Vault Secrets User" - - if [ -z "$PRINCIPAL_ID" ] || [ "$PRINCIPAL_ID" = "null" ]; then - echo "โŒ Function App managed identity principalId was not returned by infrastructure deployment" - exit 1 - fi - - # Get the Key Vault resource ID KEYVAULT_ID=$(az keyvault show \ - --name "${{ env.AZURE_KEYVAULT_NAME }}" \ - --query id \ - --output tsv) - - EXISTING_ASSIGNMENT=$(az role assignment list \ + --name "$AZURE_KEYVAULT_NAME" \ + --resource-group "$AZURE_RESOURCE_GROUP" \ + --query id --output tsv) + EXISTING=$(az role assignment list \ --scope "$KEYVAULT_ID" \ - --role "$ROLE_NAME" \ + --role "Key Vault Secrets User" \ --query "[?principalId=='$PRINCIPAL_ID'].id | [0]" \ --output tsv) - if [ -n "$EXISTING_ASSIGNMENT" ]; then - echo "โœ… $ROLE_NAME already assigned to $PRINCIPAL_ID on ${{ env.AZURE_KEYVAULT_NAME }}" + if [[ -n "$EXISTING" ]]; then exit 0 fi - # Grant Key Vault Secrets User role to the function app's managed identity. - # Newly-created system-assigned identities can take a short time to propagate. for attempt in {1..12}; do - if CREATE_OUTPUT=$(az role assignment create \ + if az role assignment create \ --assignee-object-id "$PRINCIPAL_ID" \ --assignee-principal-type ServicePrincipal \ - --role "$ROLE_NAME" \ - --scope "$KEYVAULT_ID" 2>&1); then - echo "โœ… Granted $ROLE_NAME to $PRINCIPAL_ID on ${{ env.AZURE_KEYVAULT_NAME }}" + --role "Key Vault Secrets User" \ + --scope "$KEYVAULT_ID"; then exit 0 fi - - echo "$CREATE_OUTPUT" - - if echo "$CREATE_OUTPUT" | grep -q "AuthorizationFailed"; then - echo "โŒ The GitHub Actions Azure service principal cannot create role assignments at Key Vault scope." - echo "Grant it 'User Access Administrator' at Key Vault scope, or Owner at an inherited scope, then rerun this workflow." - exit 1 - fi - - echo "Role assignment attempt $attempt failed; waiting for managed identity propagation..." sleep 10 done - - echo "โŒ Failed to grant $ROLE_NAME to $PRINCIPAL_ID on ${{ env.AZURE_KEYVAULT_NAME }}" exit 1 - - name: Deploy Alert Rules (production only) - if: needs.determine-environment.outputs.environment == 'prod' && vars.ALERT_EMAIL != '' + - name: Deploy production alert rules + if: env.DEPLOYMENT_ENVIRONMENT == 'prod' && vars.ALERT_EMAIL != '' + env: + FUNCTION_APP_NAME: ${{ steps.infrastructure.outputs.function-app-name }} run: | - echo "Deploying alert rules for production..." az deployment group create \ - --resource-group ${{ vars.AZURE_RESOURCE_GROUP }} \ + --resource-group "$AZURE_RESOURCE_GROUP" \ --template-file .azure/alert-rules.bicep \ - --name "alert-rules-$(date +%s)" \ --parameters \ - functionAppName=${{ steps.deploy-infra.outputs.functionAppName }} \ - alertEmail=${{ vars.ALERT_EMAIL }} \ - location=${{ vars.AZURE_LOCATION }} - - build-and-deploy: - permissions: - contents: read - pull-requests: write - needs: [determine-environment, provision-infrastructure] - if: needs.determine-environment.outputs.deploy == 'true' - runs-on: ubuntu-latest - environment: ${{ needs.determine-environment.outputs.environment == 'prod' && 'production' || 'development' }} - env: - PUBLISH_DIR: ${{ github.workspace }}/publish - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup .NET - uses: actions/setup-dotnet@v5 - with: - dotnet-version: ${{ env.DOTNET_VERSION }} - - - name: Restore dependencies - run: dotnet restore ${{ env.FUNCTION_PROJECT }} - - - name: Build - run: dotnet build ${{ env.FUNCTION_PROJECT }} --configuration Release --no-restore + functionAppName="$FUNCTION_APP_NAME" \ + alertEmail="$ALERT_EMAIL" \ + location="$AZURE_LOCATION" - - name: Publish - run: dotnet publish ${{ env.FUNCTION_PROJECT }} --configuration Release --no-build --output ${{ env.PUBLISH_DIR }} - - - name: Verify publish output - run: | - echo "Publish folder: ${{ env.PUBLISH_DIR }}" - ls -la ${{ env.PUBLISH_DIR }} - echo "----" - # Key files expected for Azure Functions (.NET isolated) - test -f ${{ env.PUBLISH_DIR }}/host.json || (echo "โŒ host.json missing from publish output" && exit 1) - # At least one assembly/executable should exist - if ! ls ${{ env.PUBLISH_DIR }}/*.dll 1>/dev/null 2>&1 && ! ls ${{ env.PUBLISH_DIR }}/*.exe 1>/dev/null 2>&1 ; then - echo "โŒ No binaries (*.dll/*.exe) found in publish output" && exit 1 - fi - # Helpful for .NET isolated - if [ -f ${{ env.PUBLISH_DIR }}/functions.metadata ]; then - echo "โœ… functions.metadata present" - else - echo "โš ๏ธ functions.metadata not found (may indicate missing Worker.Sdk analyzer)" - fi - if [ -f ${{ env.PUBLISH_DIR }}/worker.config.json ]; then - echo "โœ… worker.config.json present" - else - echo "โš ๏ธ worker.config.json not found" - fi - - - name: Azure Login - uses: azure/login@v2 - with: - creds: ${{ secrets.AZURE_CREDENTIALS }} - - - name: Deploy to Azure Functions + - name: Deploy application uses: Azure/functions-action@v1 with: - app-name: ${{ needs.provision-infrastructure.outputs.function-app-name }} + app-name: ${{ steps.infrastructure.outputs.function-app-name }} package: ${{ env.PUBLISH_DIR }} - - name: Get Function App URL - id: get-url + - name: Smoke test + id: smoke-test + shell: bash + env: + FUNCTION_APP_NAME: ${{ steps.infrastructure.outputs.function-app-name }} run: | - FUNCTION_URL=$(az functionapp show \ - --resource-group ${{ vars.AZURE_RESOURCE_GROUP }} \ - --name ${{ needs.provision-infrastructure.outputs.function-app-name }} \ - --query 'defaultHostName' \ - --output tsv) - echo "function-url=https://$FUNCTION_URL" >> $GITHUB_OUTPUT - echo "Function App URL: https://$FUNCTION_URL" - - - name: Smoke Test - Health Check - run: | - sleep 30 # Wait for Function App to warm up - FUNCTION_URL="${{ steps.get-url.outputs.function-url }}" - - # Try to get function list (admin endpoint) - STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$FUNCTION_URL/admin/functions") + HOSTNAME=$(az functionapp show \ + --resource-group "$AZURE_RESOURCE_GROUP" \ + --name "$FUNCTION_APP_NAME" \ + --query defaultHostName --output tsv) - if [ "$STATUS" = "401" ] || [ "$STATUS" = "200" ]; then - echo "โœ… Function App is responding (HTTP $STATUS)" - else - echo "โŒ Function App health check failed (HTTP $STATUS)" - exit 1 - fi + for attempt in {1..12}; do + STATUS=$(curl --silent --output /dev/null --write-out "%{http_code}" "https://$HOSTNAME/admin/functions") + if [[ "$STATUS" == "200" || "$STATUS" == "401" ]]; then + echo "url=https://$HOSTNAME" >> "$GITHUB_OUTPUT" + exit 0 + fi + sleep 10 + done + exit 1 - - name: Comment PR with deployment URL - if: github.event_name == 'pull_request' - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: `### ๐Ÿš€ Deployed to Azure Functions\n\n**Environment:** \`${{ needs.determine-environment.outputs.environment }}\`\n**Function App:** \`${{ needs.provision-infrastructure.outputs.function-app-name }}\`\n**URL:** ${{ steps.get-url.outputs.function-url }}\n**Network:** \`${{ needs.determine-environment.outputs.hyperliquid-network }}\`\n\n_This environment will be automatically cleaned up when the PR is closed._` - }) - - - name: Summary + - name: Deployment summary + shell: bash + env: + FUNCTION_APP_NAME: ${{ steps.infrastructure.outputs.function-app-name }} + FUNCTION_APP_URL: ${{ steps.smoke-test.outputs.url }} run: | - echo "## Deployment Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "- **Environment:** ${{ needs.determine-environment.outputs.environment }}" >> $GITHUB_STEP_SUMMARY - echo "- **Function App:** ${{ needs.provision-infrastructure.outputs.function-app-name }}" >> $GITHUB_STEP_SUMMARY - echo "- **URL:** ${{ steps.get-url.outputs.function-url }}" >> $GITHUB_STEP_SUMMARY - echo "- **Network:** ${{ needs.determine-environment.outputs.hyperliquid-network }}" >> $GITHUB_STEP_SUMMARY + { + echo "## Deployment complete" + echo "- Environment: $DEPLOYMENT_ENVIRONMENT" + echo "- Commit: $DEPLOYMENT_SHA" + echo "- Function App: $FUNCTION_APP_NAME" + echo "- URL: $FUNCTION_APP_URL" + echo "- Hyperliquid network: $HYPERLIQUID_NETWORK" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 232cb664..e1fa5b84 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -13,8 +13,6 @@ permissions: jobs: build: - # Skip environment requirement for Dependabot PRs (they can't access secrets) - environment: ${{ github.actor != 'dependabot[bot]' && 'development' || '' }} runs-on: ubuntu-latest steps: @@ -28,17 +26,6 @@ jobs: - name: Build run: dotnet build --no-restore - name: Test - # Only run integration tests if secrets are available (not Dependabot) - if: github.actor != 'dependabot[bot]' - env: - HYPERLIQUID__ADDRESS: ${{ secrets.HYPERLIQUID__ADDRESS }} - HYPERLIQUID__PRIVATEKEY: ${{ secrets.HYPERLIQUID__PRIVATEKEY }} - ROBOTWEALTH__APIKEY: ${{ secrets.ROBOTWEALTH__APIKEY }} - UNRAVEL__APIKEY: ${{ secrets.UNRAVEL__APIKEY }} - run: dotnet test --no-build --verbosity normal --collect:"XPlat Code Coverage" --results-directory ./coverage - - name: Test (Dependabot - skip integration tests) - # Run tests without secrets for Dependabot PRs - if: github.actor == 'dependabot[bot]' run: dotnet test --no-build --verbosity normal --filter "Category!=Integration" --collect:"XPlat Code Coverage" --results-directory ./coverage - name: Merge coverage reports run: | @@ -50,16 +37,3 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} file: ./coverage/merged/Cobertura.xml format: cobertura - - - name: Comment on Dependabot PR - if: github.actor == 'dependabot[bot]' && github.event_name == 'pull_request' - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: `### โœ… Build Successful (Dependabot)\n\n**Note:** Integration tests requiring secrets were skipped for security.\n\n- โœ… Build completed\n- โœ… Unit tests passed\n- โญ๏ธ Integration tests skipped (no secrets)\n- โญ๏ธ Azure deployment skipped\n\nOnce merged, full integration tests and deployment will run automatically.` - }) diff --git a/docs/AZURE-KEY-VAULT-SECRETS-SETUP.md b/docs/AZURE-KEY-VAULT-SECRETS-SETUP.md index 7193223f..20c704a4 100644 --- a/docs/AZURE-KEY-VAULT-SECRETS-SETUP.md +++ b/docs/AZURE-KEY-VAULT-SECRETS-SETUP.md @@ -1,185 +1,67 @@ # Azure Key Vault Secrets Setup -The Azure Functions deployment requires secrets to be stored in Azure Key Vault. The secrets are environment-specific (dev vs prod) and are automatically referenced by the function app based on the deployment environment. +Development and production have separate Key Vaults. Both vaults use the same secret names; the selected GitHub environment determines which vault is referenced. -## Required Secrets +The vault data-plane endpoint intentionally remains publicly reachable because GitHub-hosted runners and operator workstations do not have stable egress IP addresses. Public reachability does not grant access: Entra authentication and vault-scoped Azure RBAC are required. If private endpoints or static-egress runners are introduced later, change `publicNetworkAccess` and `networkAcls` together and provide operators an approved private access path before disabling public access. -### Hyperliquid Agent Wallet Secrets (Development/Testnet) +## Required secrets -Used for dev, PR, and feature branch deployments. The agent wallet provides API credentials for signing transactions: +Populate each vault with: -```bash -# Development agent wallet address -az keyvault secret set \ - --vault-name \ - --name "hyperliquid-dev-agent-address" \ - --value "" - -# Development agent private key -az keyvault secret set \ - --vault-name \ - --name "hyperliquid-dev-agent-privatekey" \ - --value "" -``` +| Secret | Purpose | +| --- | --- | +| `hyperliquid-agent-address` | Hyperliquid signing-agent address | +| `hyperliquid-agent-privatekey` | Hyperliquid signing-agent private key | +| `hyperliquid-vault-yolodaily` | Funded account for YoloDaily | +| `hyperliquid-vault-unraveldaily` | Funded account for UnravelDaily | +| `robotwealth-api-key` | RobotWealth API key used by that environment | +| `unravel-api-key` | Unravel API key used by that environment | -### Hyperliquid Agent Wallet Secrets (Production/Mainnet) +The development vault must contain testnet credentials only. Never copy a mainnet private key or funded mainnet vault address into development. -Used for production deployments. The agent wallet provides API credentials for signing transactions: +Example, repeated once for each environment-specific vault: ```bash -# Production agent wallet address -az keyvault secret set \ - --vault-name \ - --name "hyperliquid-prod-agent-address" \ - --value "" - -# Production agent private key -az keyvault secret set \ - --vault-name \ - --name "hyperliquid-prod-agent-privatekey" \ - --value "" +VAULT_NAME="" + +az keyvault secret set --vault-name "$VAULT_NAME" --name hyperliquid-agent-address --value "
" +az keyvault secret set --vault-name "$VAULT_NAME" --name hyperliquid-agent-privatekey --value "" +az keyvault secret set --vault-name "$VAULT_NAME" --name hyperliquid-vault-yolodaily --value "" +az keyvault secret set --vault-name "$VAULT_NAME" --name hyperliquid-vault-unraveldaily --value "" +az keyvault secret set --vault-name "$VAULT_NAME" --name robotwealth-api-key --value "" +az keyvault secret set --vault-name "$VAULT_NAME" --name unravel-api-key --value "" ``` -### Hyperliquid Vault Addresses (Strategy-Specific) +Avoid placing secret values in shell history where practical; the commands above show the required names, not a preferred secret-entry mechanism. -Each strategy requires a vault address (the funded account where trades are executed). Replace `{strategy}` with your strategy name in lowercase (e.g., `yolodaily`, `unraveldaily`): +## Access model -```bash -# Development vault address for a specific strategy -az keyvault secret set \ - --vault-name \ - --name "hyperliquid-dev-vault-{strategy}" \ - --value "" - -# Production vault address for a specific strategy -az keyvault secret set \ - --vault-name \ - --name "hyperliquid-prod-vault-{strategy}" \ - --value "" -``` +Each Function App has a system-assigned managed identity. The deployment workflow grants that identity `Key Vault Secrets User` on its own vault only. No cross-environment role assignments should exist. -Example for specific strategies: +Verify isolation after both apps exist: ```bash -# YoloDaily strategy vaults -az keyvault secret set --vault-name YOLO --name "hyperliquid-dev-vault-yolodaily" --value "
" -az keyvault secret set --vault-name YOLO --name "hyperliquid-prod-vault-yolodaily" --value "
" - -# UnravelDaily strategy vaults -az keyvault secret set --vault-name YOLO --name "hyperliquid-dev-vault-unraveldaily" --value "
" -az keyvault secret set --vault-name YOLO --name "hyperliquid-prod-vault-unraveldaily" --value "
" -``` - -### API Keys - -These are shared across all environments: +DEV_PRINCIPAL=$(az functionapp identity show --resource-group "" --name yolo-funk-dev --query principalId -o tsv) +PROD_PRINCIPAL=$(az functionapp identity show --resource-group "" --name yolo-funk-prod --query principalId -o tsv) -```bash -# RobotWealth API Key -az keyvault secret set \ - --vault-name \ - --name "robotwealth-api-key" \ - --value "" - -# Unravel API Key -az keyvault secret set \ - --vault-name \ - --name "unravel-api-key" \ - --value "" +az role assignment list --assignee "$DEV_PRINCIPAL" --all -o table +az role assignment list --assignee "$PROD_PRINCIPAL" --all -o table ``` -## Secret Naming Convention - -Secrets use lowercase with hyphens as separators: - -- `hyperliquid-dev-agent-address` โ†’ Development agent wallet address -- `hyperliquid-prod-agent-address` โ†’ Production agent wallet address -- `hyperliquid-dev-vault-{strategy}` โ†’ Development vault for specific strategy -- `hyperliquid-prod-vault-{strategy}` โ†’ Production vault for specific strategy -- `robotwealth-api-key` โ†’ RobotWealth API key (shared) -- `unravel-api-key` โ†’ Unravel API key (shared) - -## Hyperliquid Wallet Architecture - -Hyperliquid uses a two-wallet system: - -1. **Agent Wallet**: API credentials (address + private key) used for signing transactions - - Stored as: `hyperliquid-{env}-agent-address` and `hyperliquid-{env}-agent-privatekey` -2. **Vault Address**: The actual funded account where your capital is deposited - - Stored as: `hyperliquid-{env}-vault-{strategy}` - - Strategy-specific (e.g., different vaults for yolodaily vs unraveldaily) - -Both are required for each environment. - -## Environment-Based Secret Selection - -The deployment automatically selects the correct secrets based on the `hyperliquidNetwork` parameter: - -- **Development/Testnet**: `dev`, `pr-*`, `feat-*` โ†’ uses `hyperliquid-dev-*` secrets -- **Production/Mainnet**: `prod`, `master` branch โ†’ uses `hyperliquid-prod-*` secrets - -The Bicep template maps `hyperliquidNetwork` to secret environment: +The development principal should list only the development vault assignment, and the production principal only the production vault assignment. -- `testnet` โ†’ `dev` secrets -- `mainnet` โ†’ `prod` secrets +## Migrating the production vault -## Automated Setup +1. Create or select the dedicated production vault in the production resource group. +2. Copy values from the legacy environment-qualified secrets into the new neutral names. +3. Set the production GitHub environment variable `AZURE_KEYVAULT_NAME` to the dedicated vault. +4. Run the protected production deployment and confirm every Function App Key Vault reference reports a resolved status. +5. Verify application startup and strategy configuration before disabling or deleting legacy secrets. -The easiest way to configure all secrets is to use the setup script: - -```bash -./scripts/setup-azure.sh -``` - -This script will: - -- Check which secrets already exist -- Prompt for missing secrets only -- Support multiple strategies per environment -- Save credentials securely to Key Vault - -## Granting Access - -The GitHub Actions workflow automatically grants the function app's managed identity access to the Key Vault. If this fails, you can manually grant access: - -```bash -# Get the function app's managed identity principal ID -PRINCIPAL_ID=$(az functionapp show \ - --resource-group ResourceGroup1 \ - --name \ - --query identity.principalId -o tsv) - -# Grant Key Vault Secrets User role -az role assignment create \ - --assignee-object-id $PRINCIPAL_ID \ - --assignee-principal-type ServicePrincipal \ - --role "Key Vault Secrets User" \ - --scope /subscriptions//resourceGroups/ResourceGroup1/providers/Microsoft.KeyVault/vaults/ -``` - -## Verification - -After deploying, verify the secrets are accessible: - -1. Check the function app configuration in Azure Portal -2. Look for app settings starting with `Strategies__` that have values like `@Microsoft.KeyVault(...)` -3. Check the function app logs for any Key Vault access errors +Do not delete the old vault or remove its role assignments as part of the first migration deployment. Retain it until production has run successfully with the new references. ## Troubleshooting -**Function app fails to start with "No script host available":** - -- Verify all required secrets exist in Key Vault -- Check the function app's managed identity has "Key Vault Secrets User" role -- Review Application Insights or function app logs for detailed error messages - -**404 when calling function endpoints:** - -- Function app may be failing to start due to missing secrets -- Check `LogFiles/Application/Functions/Host/*.log` in the Kudu console - -**Secret reference not resolving:** - -- Verify secret name matches exactly (case-sensitive, use hyphens not underscores) -- Ensure Key Vault URI format: `@Microsoft.KeyVault(SecretUri=https://{vault}.vault.azure.net/secrets/{secret}/)` -- Check managed identity has appropriate permissions +- A Key Vault reference error usually means a missing secret name or missing `Key Vault Secrets User` assignment. +- A deployment failure while granting access means the deployment identity lacks User Access Administrator or Owner at the vault/resource-group scope. +- A development app configured for mainnet indicates a branch/environment mismatch; the workflow should reject this before Azure login. diff --git a/docs/CONFIGURATION-ARCHITECTURE.md b/docs/CONFIGURATION-ARCHITECTURE.md index 7770111b..f9b69059 100644 --- a/docs/CONFIGURATION-ARCHITECTURE.md +++ b/docs/CONFIGURATION-ARCHITECTURE.md @@ -93,19 +93,19 @@ Only secrets for local Azure Functions development: **Note:** Double-underscore `__` syntax is required for Azure Functions local CLI. -### Azure App Settings (Production) +### Azure App Settings -Only override secrets - all other config comes from appsettings.json in deployment: +Only override secrets; all other config comes from `appsettings.json`. Development and production use separate Key Vaults with identical secret names: ```bash az functionapp config appsettings set \ --name yolo-funk-prod \ - --resource-group ResourceGroup1 \ + --resource-group \ --settings \ - "Strategies__YoloDaily__Hyperliquid__Address=@Microsoft.KeyVault(VaultName=YOLO;SecretName=hyperliquid-prod-agent-address)" \ - "Strategies__YoloDaily__Hyperliquid__PrivateKey=@Microsoft.KeyVault(VaultName=YOLO;SecretName=hyperliquid-prod-agent-privatekey)" \ - "Strategies__YoloDaily__Hyperliquid__VaultAddress=@Microsoft.KeyVault(VaultName=YOLO;SecretName=hyperliquid-prod-vault-yolodaily)" \ - "Strategies__YoloDaily__RobotWealth__ApiKey=@Microsoft.KeyVault(VaultName=YOLO;SecretName=robotwealth-api-key)" + "Strategies__YoloDaily__Hyperliquid__Address=@Microsoft.KeyVault(VaultName=;SecretName=hyperliquid-agent-address)" \ + "Strategies__YoloDaily__Hyperliquid__PrivateKey=@Microsoft.KeyVault(VaultName=;SecretName=hyperliquid-agent-privatekey)" \ + "Strategies__YoloDaily__Hyperliquid__VaultAddress=@Microsoft.KeyVault(VaultName=;SecretName=hyperliquid-vault-yolodaily)" \ + "Strategies__YoloDaily__RobotWealth__ApiKey=@Microsoft.KeyVault(VaultName=;SecretName=robotwealth-api-key)" ``` ## Configuration Merge Order diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index dbe44793..e20238e1 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -1,318 +1,62 @@ # Deployment Guide -This document describes how to deploy the YOLO trading application to Azure Functions using automated CI/CD pipelines. +YOLO uses two long-lived Azure environments. Pull requests run CI only and never receive Azure credentials or create Azure resources. -## Table of Contents +| Environment | Source branch | Function App | Hyperliquid | Deployment | +| --- | --- | --- | --- | --- | +| Development | `develop` | `yolo-funk-dev` | Testnet | Automatic after merge | +| Production | `master` | `yolo-funk-prod` | Mainnet | Starts after release merge; requires approval | -1. [Overview](#overview) -2. [Environments](#environments) -3. [Prerequisites](#prerequisites) -4. [Initial Setup](#initial-setup) -5. [Configuration](#configuration) -6. [Deployment Process](#deployment-process) -7. [Monitoring](#monitoring) +## GitHub setup -## Overview +Create GitHub environments named `development` and `production`. Configure each with environment-scoped values so credentials and resource names cannot cross environments: -The YOLO trading application uses a GitOps approach with automated deployments: +- Secret: `AZURE_CREDENTIALS` +- Variables: `AZURE_RESOURCE_GROUP`, `AZURE_LOCATION`, `AZURE_KEYVAULT_NAME` +- Production-only optional variable: `ALERT_EMAIL` -- **Feature branches / PRs** โ†’ Ephemeral test environments (auto-created, auto-deleted) -- **`master` branch** โ†’ Production environment (Hyperliquid mainnet, requires approval) +Add a required reviewer to `production`. Do not add a deployment approval to `development`. -All infrastructure is provisioned automatically using Azure Bicep templates. +Create `develop` from `master` and make it the default branch. Protect both branches: -## Environments +- Require the `.NET / build` check and at least one approving review. +- Prevent direct pushes and force pushes. +- Target normal and Dependabot pull requests at `develop`. +- Use PRs from `develop` to `master` as production releases. +- Merge release PRs with a merge commit; the production workflow rejects direct, squash, and rebase commits on `master`. -| Environment | Branch | Azure Function App | Hyperliquid Network | Auto-Deploy | Auto-Cleanup | -| ------------ | ----------------- | ----------------------- | ------------------- | ---------------------- | -------------- | -| PR / Feature | `feature/*` or PR | `yolo-funk-pr-{number}` | testnet | โœ… | โœ… on PR close | -| Production | `master` | `yolo-funk-prod` | mainnet | โœ… (approval required) | โŒ | +The deployment identity for each environment needs Contributor on only its resource group and User Access Administrator on that resource group (or its Key Vault). The latter allows the workflow to grant `Key Vault Secrets User` to that environment's Function App identity. -## Prerequisites +## Normal deployment flow -- Azure subscription -- Azure CLI installed locally -- GitHub repository with Actions enabled -- .NET 10.0 SDK +1. Open a PR against `develop`. CI builds and runs tests excluding the `Integration` category; no Azure deployment occurs. +2. Merge the PR. The exact resulting `develop` SHA deploys automatically to the development resource group and `yolo-funk-dev`. +3. Observe testnet execution, Key Vault resolution, timers, storage, and telemetry. +4. Open a release PR from `develop` to `master`. +5. Merge the release PR. The production job waits at the protected `production` environment before any checkout, infrastructure change, or code deployment occurs. +6. Approve the deployment. The workflow checks out and verifies the exact merge SHA, then deploys it to `yolo-funk-prod`. -## Initial Setup +Manual workflow dispatch is retained for redeployment. Select the workflow from `develop` when deploying `dev`, or from `master` when deploying `prod`; mismatched branch/environment combinations fail before Azure login. Production dispatches still require approval. -### 1. Create Azure Service Principal +## Azure layout -Create a service principal for GitHub Actions to authenticate with Azure: +Development and production use separate resource groups in the same subscription. Each resource group contains its own Function App, Consumption plan, storage account, managed identity, Key Vault, Log Analytics workspace, and Application Insights instance. -```bash -# Login to Azure -az login +The Bicep templates create infrastructure, including an empty RBAC-enabled Key Vault. Secret values must be populated separately before the first application deployment; see [Azure Key Vault Secrets Setup](AZURE-KEY-VAULT-SECRETS-SETUP.md). -# Set your subscription -az account set --subscription "Your Subscription Name" +Globally scoped names must be unique. In particular, choose distinct `AZURE_KEYVAULT_NAME` values. The Function App names are fixed as `yolo-funk-dev` and `yolo-funk-prod`, while storage names are deterministically unique per resource group. -# Create service principal with contributor role -az ad sp create-for-rbac \ - --name "github-yolo-funk" \ - --role contributor \ - --scopes /subscriptions/{subscription-id} \ - --sdk-auth +## Migration order -# Output will be JSON - copy this entire output -``` +1. Create and configure the `development` GitHub environment and its dedicated Azure identity, resource group name, location, and globally unique Key Vault name. +2. Provision the development Key Vault, populate testnet-only secrets, and deploy `develop`. +3. Confirm the development managed identity has access only to its own vault and validate testnet behavior and telemetry. +4. Create the `production` GitHub environment. Point it at the existing production resource group initially so the current Function App is updated in place, not recreated. +5. Create/populate the production-only vault with the environment-neutral secret names. Do not remove the legacy secrets until the new references resolve successfully. +6. Merge a release PR and approve production. Verify the deployed SHA and live health before retiring the legacy shared-vault configuration. -### 2. Configure GitHub Secrets +## Rollback -Add the following secrets to your GitHub repository (`Settings` โ†’ `Secrets and variables` โ†’ `Actions`): +Production always deploys a commit on `master`. To roll back, revert the problematic release commit (or create a PR restoring the known-good state), merge it into `master`, and approve the resulting production deployment. This preserves an auditable history and prevents arbitrary unreviewed commits from reaching mainnet. -| Secret Name | Value | Description | -| ------------------- | --------------------------- | ------------------------------------------------ | -| `AZURE_CREDENTIALS` | JSON from service principal | Full JSON output from `az ad sp create-for-rbac` | - -### 3. Configure GitHub Variables (optional) - -Add these variables if using Azure Key Vault: - -| Variable Name | Value | Description | -| --------------------- | ------------------ | ------------------------- | -| `AZURE_KEYVAULT_NAME` | e.g., `yolo-vault` | Your Azure Key Vault name | - -### 4. Create Azure Key Vault (optional but recommended) - -```bash -# Create Key Vault -az keyvault create \ - --name yolo-vault \ - --resource-group rg-yolo-funk \ - --location australiaeast - -# Add API secrets as applicable -az keyvault secret set \ - --vault-name yolo-vault \ - --name "robotwealth-api-key" \ - --value "YourApiKey" - -az keyvault secret set \ - --vault-name yolo-vault \ - --name "unravel-api-key" \ - --value "YourApiKey" - -# Add secrets for development environment -az keyvault secret set \ - --vault-name yolo-vault \ - --name "hyperliquid-dev-agent-address" \ - --value "0xYourTestnetAgentAddress" - -az keyvault secret set \ - --vault-name yolo-vault \ - --name "hyperliquid-dev-agent-privatekey" \ - --value "YourTestnetAgentPrivateKey" - -# Add secrets for production environment -az keyvault secret set \ - --vault-name yolo-vault \ - --name "hyperliquid-prod-agent-address" \ - --value "0xYourMainnetAgentAddress" - -az keyvault secret set \ - --vault-name yolo-vault \ - --name "hyperliquid-prod-agent-privatekey" \ - --value "YourMainnetAgentPrivateKey" - -az keyvault secret set \ - --vault-name yolo-vault \ - --name "hyperliquid-prod-vaultaddress" \ - --value "OxYourMainnetHyperliquidSubaccount" -``` - -### 5. Create GitHub Environments (for manual approvals) - -1. Go to `Settings` โ†’ `Environments` โ†’ `New environment` -2. Create environment named `production` -3. Enable "Required reviewers" and add yourself -4. (Optional) Create `development` environment without restrictions - -## Configuration - -### Application Settings - -After first deployment, configure each Function App with application settings. Settings can be added via: - -1. **Azure Portal**: Function App โ†’ Configuration โ†’ Application settings -2. **Azure CLI**: See examples below - -#### Development/PR Environment Settings - -Probably no overrides required - other than schedule MUST be set for each strategy or else the timed job will fail, if testing thereof is desired: - -```bash -FUNCTION_APP="yolo-funk-pr-83" - -az functionapp config appsettings set \ - --name $FUNCTION_APP \ - --resource-group rg-yolo-funk \ - --settings \ - "Strategies__MomentumDaily__Schedule=*/5 * * * *" -``` - -#### Production Environment Settings - -Can be overridden as desired - schedule MUST be set for each strategy or else the timed job will fail: - -```bash -FUNCTION_APP="yolo-funk-prod" - -az functionapp config appsettings set \ - --name $FUNCTION_APP \ - --resource-group rg-yolo-funk \ - --settings \ - "Strategies__MomentumDaily__Yolo__MaxLeverage=3.0" \ - "Strategies__MomentumDaily__Yolo__NotionalCash=20000" \ - "Strategies__MomentumDaily__Schedule=0 30 0 * * *" -``` - -### Local Development - -Create `src/YoloFunk/local.settings.json` (git-ignored): - -```json -{ - "IsEncrypted": false, - "Values": { - "AzureWebJobsStorage": "UseDevelopmentStorage=true", - "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", - "Strategies__MomentumDaily__Yolo__MaxLeverage": "1.0", - "Strategies__MomentumDaily__Yolo__NotionalCash": "1000", - "Strategies__MomentumDaily__Hyperliquid__Dev__Agent__Address": "0xYourTestnetAddress", - "Strategies__MomentumDaily__Hyperliquid__Dev__Agent__PrivateKey": "YourTestnetPrivateKey", - "Strategies__MomentumDaily__RobotWealth__ApiKey": "YourApiKey", - "Strategies__MomentumDaily__Unravel__ApiKey": "YourApiKey", - "Strategies__MomentumDaily__Schedule": "0 */5 * * * *" - } -} -``` - -## Deployment Process - -### Automatic Deployments - -#### Feature Branch / Pull Request - -1. Create feature branch: `git checkout -b feature/my-new-feature` -2. Make changes and push: `git push -u origin feature/my-new-feature` -3. Create pull request on GitHub -4. **Automatic**: Infrastructure provisioned and code deployed to `yolo-funk-pr-{number}` -5. PR receives comment with deployment URL -6. Test your changes in isolated environment -7. When PR is closed/merged to `master`: **Automatic cleanup** deletes all resources - -#### Production Environment - -1. Merge to `master` branch -2. **Automatic**: Starts deployment workflow -3. **Manual approval required** (via GitHub Environment protection) -4. After approval: Deploys to `yolo-funk-prod` (mainnet) - -### Manual Deployments - -Trigger deployment manually via GitHub Actions: - -1. Go to `Actions` โ†’ `Deploy to Azure Functions` โ†’ `Run workflow` -2. Select environment: `dev` or `prod` -3. Click `Run workflow` - -### Manual Cleanup - -To cleanup a specific environment: - -1. Go to `Actions` โ†’ `Cleanup Azure Functions` โ†’ `Run workflow` -2. Enter environment name (e.g., `pr-123`, `feat-my-feature`) -3. Click `Run workflow` - -Note: Production (`prod`) cannot be cleaned up via automation for safety. - -## Monitoring - -### View Logs - -#### Azure Portal - -1. Navigate to Function App -2. Go to `Log stream` or `Monitoring` โ†’ `Logs` -3. Query Application Insights - -#### Azure CLI - -```bash -# Get recent logs -az monitor app-insights query \ - --app yolo-funk-insights \ - --resource-group rg-yolo-funk \ - --analytics-query "traces | where timestamp > ago(1h) | order by timestamp desc | take 100" - -# Get errors -az monitor app-insights query \ - --app yolo-funk-insights \ - --resource-group rg-yolo-funk \ - --analytics-query "exceptions | where timestamp > ago(24h) | order by timestamp desc" -``` - -### Metrics - -View metrics in Azure Portal โ†’ Application Insights โ†’ `yolo-funk-insights`: - -- Request rates -- Failure rates -- Response times -- Custom events from your trading strategies - -### Costs - -Monitor costs via Azure Portal โ†’ Cost Management: - -- Consumption Plan: Pay per execution (~$0.20 per million executions) -- Storage: ~$0.02 per GB per month -- Application Insights: First 5GB/month free - -Ephemeral PR environments are automatically cleaned up to minimize costs. - -## Troubleshooting - -### Deployment Fails - -1. Check GitHub Actions logs for detailed error messages -2. Verify Azure credentials are valid: `az login` and test commands -3. Ensure Resource Group exists: `az group show --name rg-yolo-funk` - -### Function App Not Starting - -1. Check Application Insights logs for errors -2. Verify configuration settings (especially Key Vault references) -3. Check that managed identity has Key Vault access: - - ```bash - az functionapp identity show --name yolo-funk-dev --resource-group rg-yolo-funk - az keyvault show --name yolo-vault --query properties.accessPolicies - ``` - -### Key Vault Access Denied - -Grant Function App managed identity access: - -```bash -# Get Function App principal ID -PRINCIPAL_ID=$(az functionapp identity show \ - --name yolo-funk-dev \ - --resource-group rg-yolo-funk \ - --query principalId \ - --output tsv) - -# Grant access -az keyvault set-policy \ - --name yolo-vault \ - --object-id $PRINCIPAL_ID \ - --secret-permissions get list -``` - -## Additional Resources - -- [Azure Functions Documentation](https://docs.microsoft.com/azure/azure-functions/) -- [Azure Bicep Documentation](https://docs.microsoft.com/azure/azure-resource-manager/bicep/) -- [GitHub Actions Documentation](https://docs.github.com/actions) +Leaving or rejecting a production approval does not modify Azure; the existing production deployment continues running. diff --git a/docs/HYPERLIQUID-WALLET-ARCHITECTURE.md b/docs/HYPERLIQUID-WALLET-ARCHITECTURE.md index 33427c8d..7616d4e8 100644 --- a/docs/HYPERLIQUID-WALLET-ARCHITECTURE.md +++ b/docs/HYPERLIQUID-WALLET-ARCHITECTURE.md @@ -1,207 +1,43 @@ # Hyperliquid Wallet Architecture -## Overview +Hyperliquid uses two distinct identities: -Hyperliquid uses a two-tier wallet structure for security: +1. The **agent wallet** signs API transactions and should hold no funds. +2. The **vault address** identifies the funded account whose positions are managed. -1. **Agent/API Wallet** - Signs transactions but holds no funds -2. **Vault Address** - Actual funded account (hardware wallet for production) +Production should use a hardware-backed owner wallet and separate funded vaults/subaccounts for YoloDaily and UnravelDaily. The agent can be rotated without moving funds; the owner-wallet private key must never be exposed to the application. -## Architecture +## Environment isolation -``` -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ Production Setup โ”‚ -โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค -โ”‚ โ”‚ -โ”‚ Hardware Wallet (0xVault...) โ”‚ -โ”‚ โ””โ”€ Holds all funds โ”‚ -โ”‚ โ””โ”€ Distributed to sub-accounts via Hyperliquid web UI โ”‚ -โ”‚ โ””โ”€ Sub-account 1 (Strategy: YOLO Daily). โ”‚ -โ”‚ โ””โ”€ Sub-account 2 (Strategy: Unravel Daily) โ”‚ -โ”‚ โ”‚ -โ”‚ Agent Wallet (0xAgent...) โ”‚ -โ”‚ โ””โ”€ No funds deposited โ”‚ -โ”‚ โ””โ”€ Used only for API authentication & transaction signing โ”‚ -โ”‚ โ””โ”€ References vault address when trading โ”‚ -โ”‚ โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -``` - -## Configuration - -### Azure Key Vault Secrets - -For each environment (dev/prod), store: - -**Agent Wallet (one per environment):** - -```bash -# Development -az keyvault secret set --vault-name YOLO --name "hyperliquid-dev-agent-address" --value "0x..." -az keyvault secret set --vault-name YOLO --name "hyperliquid-dev-agent-privatekey" --value "0x..." +Development and production have separate Azure Key Vaults. Development contains only Hyperliquid testnet credentials; production contains only mainnet credentials. Both vaults use the same names: -# Production -az keyvault secret set --vault-name YOLO --name "hyperliquid-prod-agent-address" --value "0x..." -az keyvault secret set --vault-name YOLO --name "hyperliquid-prod-agent-privatekey" --value "0x..." -``` - -**Vault Addresses (one per strategy per environment):** - -```bash -# Development - Strategy: YOLO daily -az keyvault secret set --vault-name YOLO --name "hyperliquid-dev-vault-yolodaily" --value "0x..." - -# Development - Strategy: Unravel daily -az keyvault secret set --vault-name YOLO --name "hyperliquid-dev-vault-unraveldaily" --value "0x..." - -# Production - Strategy: YOLO daily -az keyvault secret set --vault-name YOLO --name "hyperliquid-prod-vault-yolodaily" --value "0x..." - -# Production - Strategy: Unravel daily -az keyvault secret set --vault-name YOLO --name "hyperliquid-prod-vault-unraveldaily" --value "0x..." -``` +- `hyperliquid-agent-address` +- `hyperliquid-agent-privatekey` +- `hyperliquid-vault-yolodaily` +- `hyperliquid-vault-unraveldaily` -### Azure Function App Settings +See [Azure Key Vault Secrets Setup](AZURE-KEY-VAULT-SECRETS-SETUP.md) for commands and access verification. -Reference these secrets in your Function App configuration: - -```bash -az functionapp config appsettings set \ - --name yolo-funk-prod \ - --resource-group ResourceGroup1 \ - --settings \ - "Strategies__YoloDaily__Hyperliquid__Address=@Microsoft.KeyVault(VaultName=YOLO;SecretName=hyperliquid-prod-agent-address)" \ - "Strategies__YoloDaily__Hyperliquid__PrivateKey=@Microsoft.KeyVault(VaultName=YOLO;SecretName=hyperliquid-prod-agent-privatekey)" \ - "Strategies__YoloDaily__Hyperliquid__VaultAddress=@Microsoft.KeyVault(VaultName=YOLO;SecretName=hyperliquid-prod-vault-yolodaily)" \ - "Strategies__YoloDaily__RobotWealth__ApiKey=@Microsoft.KeyVault(VaultName=YOLO;SecretName=robotwealth-api-key)" -``` - -### Code Usage - -The HyperliquidConfig needs to include the vault address: +At runtime, Bicep maps those secrets to each strategy's `HyperliquidConfig`: ```csharp public class HyperliquidConfig { - public string Address { get; set; } // Agent wallet address (for signing) - public string PrivateKey { get; set; } // Agent wallet private key (for signing) - public string? VaultAddress { get; set; } // Actual funded vault address + public string Address { get; set; } // Agent address + public string PrivateKey { get; set; } // Agent signing key + public string? VaultAddress { get; set; } // Funded strategy vault + public bool UseTestnet { get; set; } } ``` -When placing orders, use the `vaultAddress` parameter: - -```csharp -var client = new HyperLiquidRestClient(options => { - options.ApiCredentials = new ApiCredentials( - hyperliquidConfig.Address, // Agent wallet (signs txs) - hyperliquidConfig.PrivateKey // Agent wallet key - ); -}); - -// When trading, reference the vault -var order = new OrderRequest { - Asset = "BTC-PERP", - IsBuy = true, - Quantity = 0.1m, - Price = 50000m, - VaultAddress = hyperliquidConfig.VaultAddress // The funded account -}; -``` - -## Security Benefits - -โœ… **Agent wallet exposed to API** - Can be rotated without moving funds -โœ… **Vault wallet never exposed** - Funds stay safe in hardware wallet -โœ… **Sub-account isolation** - Different strategies can't affect each other's positions -โœ… **Easy credential rotation** - Generate new agent wallet, update Key Vault, restart Function App - -## Setup Steps - -### 1. Create Agent Wallet (Per Environment) - -```bash -# Generate new agent wallet for dev -./generate-eth-keypair.py - -# Store in Key Vault -az keyvault secret set --vault-name YOLO --name "hyperliquid-dev-agent-address" --value "0xGenerated..." -az keyvault secret set --vault-name YOLO --name "hyperliquid-dev-agent-privatekey" --value "0xGenerated..." -``` - -### 2. Create Hardware Wallet (Production Only) - -- Use Ledger, Trezor, or similar hardware wallet -- **Never** expose the private key -- Fund this wallet with your trading capital - -### 3. Create Sub-Accounts (Via Hyperliquid Web UI) - -1. Connect hardware wallet to https://app.hyperliquid.xyz -2. Navigate to "Vaults" or "Sub-Accounts" -3. Create sub-account for each strategy (e.g., "Momentum Daily") -4. Allocate funds to each sub-account -5. Copy the sub-account address - -### 4. Store Vault Addresses - -```bash -# For each strategy -az keyvault secret set \ - --vault-name YOLO \ - --name "hyperliquid-prod-vault-yolodaily" \ - --value "0xSubAccountAddress..." -``` - -### 5. Authorize Agent Wallet - -In Hyperliquid web UI: - -1. Go to Settings โ†’ API -2. Add agent wallet address as authorized API key -3. Grant permissions: "Place Orders", "Cancel Orders", "View Positions" - -## Development vs Production - -| | Development (Testnet) | Production (Mainnet) | -| ---------------- | ----------------------------------- | ------------------------------- | -| **Agent Wallet** | Generated locally | Generated locally | -| **Vault Wallet** | Test wallet (can be regular wallet) | Hardware wallet (Ledger/Trezor) | -| **Funds** | Testnet tokens (free) | Real money | -| **Sub-accounts** | Optional | Recommended per strategy | +The development deployment forces `UseTestnet=true`; production forces `UseTestnet=false`. Branch and environment validation in the deployment workflow prevents selecting mainnet from `develop` or testnet from `master`. -## Automated Setup +## Setup -Run the setup script - it will prompt for all required credentials: +1. Generate a different agent wallet for each environment with `scripts/generate-eth-keypair.py`. +2. Authorize the development agent on Hyperliquid testnet and the production agent on mainnet. +3. Create separate vaults/subaccounts for each strategy. +4. Store agent and vault values in the appropriate environment's Key Vault. +5. Confirm the agent wallet holds no funds and each Function App identity can read only its own Key Vault. -```bash -./setup-azure.sh -``` - -The script will: - -- Create agent wallet secrets (one per environment) -- Prompt for vault addresses per strategy -- Store everything in Azure Key Vault -- Generate Azure CLI commands for Function App configuration - -## Manual Commands - -List all secrets: - -```bash -az keyvault secret list --vault-name YOLO --query "[].name" -o table -``` - -View a secret value: - -```bash -az keyvault secret show --vault-name YOLO --name "hyperliquid-prod-vault-yolodaily" --query "value" -o tsv -``` - -Update a secret: - -```bash -az keyvault secret set --vault-name YOLO --name "secret-name" --value "new-value" -``` +Never reuse the production agent private key, vault address, or owner-wallet credentials in development. diff --git a/scripts/generate-eth-keypair.py b/scripts/generate-eth-keypair.py index 7601ae29..ab16a4a8 100755 --- a/scripts/generate-eth-keypair.py +++ b/scripts/generate-eth-keypair.py @@ -46,39 +46,23 @@ def main(): print("1. NEVER share your private key with anyone") print("2. NEVER commit your private key to source control") print("3. Store the private key securely in Azure Key Vault") - print("4. This wallet has NO FUNDS - you must fund it before use") + print("4. Keep this agent wallet unfunded; strategy funds belong in the vault") print() print("๐Ÿ“ Next Steps:") print("-" * 60) - print("For TESTNET:") - print(" 1. Fund this address with testnet tokens") - print(" 2. Store credentials in Azure Key Vault:") + print("Store this agent in the dedicated Key Vault for the intended environment:") print() - print(f" az keyvault secret set \\") - print(f" --vault-name YOLO \\") - print(f" --name hyperliquid-dev-address \\") + print(" az keyvault secret set \\") + print(" --vault-name \\") + print(" --name hyperliquid-agent-address \\") print(f" --value '{keypair['address']}'") print() - print(f" az keyvault secret set \\") - print(f" --vault-name YOLO \\") - print(f" --name hyperliquid-dev-privatekey \\") - print(f" --value '{keypair['private_key']}'") - print() - print("For MAINNET:") - print(" โš ๏ธ Use a hardware wallet or secure key management!") - print(" 1. Fund this address with REAL tokens (be careful!)") - print(" 2. Store credentials in Azure Key Vault:") - print() - print(f" az keyvault secret set \\") - print(f" --vault-name YOLO \\") - print(f" --name hyperliquid-prod-address \\") - print(f" --value '{keypair['address']}'") - print() - print(f" az keyvault secret set \\") - print(f" --vault-name YOLO \\") - print(f" --name hyperliquid-prod-privatekey \\") + print(" az keyvault secret set \\") + print(" --vault-name \\") + print(" --name hyperliquid-agent-privatekey \\") print(f" --value '{keypair['private_key']}'") print() + print("Generate separate agents for testnet and mainnet; never reuse credentials across vaults.") print("=" * 60) diff --git a/scripts/setup-azure.sh b/scripts/setup-azure.sh index 8a6fc1a8..39b343f0 100755 --- a/scripts/setup-azure.sh +++ b/scripts/setup-azure.sh @@ -1,332 +1,62 @@ -#!/bin/bash +#!/usr/bin/env bash -# Setup script for Azure Functions deployment -# This script helps you configure all necessary Azure resources and GitHub secrets +set -euo pipefail -set -e - -echo "๐Ÿš€ YOLO Azure Functions Deployment Setup" -echo "========================================" -echo "" - -# Check prerequisites -command -v az >/dev/null 2>&1 || { echo "โŒ Azure CLI is required but not installed. Visit https://aka.ms/InstallAzureCLI"; exit 1; } -command -v gh >/dev/null 2>&1 || { echo "โš ๏ธ GitHub CLI not installed. You'll need to manually add secrets to GitHub."; GH_INSTALLED=false; } - -# Configuration (matching your existing Azure resources) -RESOURCE_GROUP="ResourceGroup1" -LOCATION="switzerlandnorth" # Azure uses lowercase, no spaces -KEYVAULT_NAME="YOLO" -SP_NAME="github-yolo-funk" - -echo "Configuration:" -echo " Resource Group: $RESOURCE_GROUP" -echo " Location: $LOCATION" -echo " Key Vault: $KEYVAULT_NAME" -echo "" -echo "NOTE: This script will use your existing Resource Group and Key Vault." -echo " It will only create what doesn't already exist." -echo "" - -read -p "Continue with this configuration? (y/n) " -n 1 -r -echo -if [[ ! $REPLY =~ ^[Yy]$ ]]; then - echo "Aborted." - exit 1 -fi - -# Login to Azure -echo "" -echo "Step 1: Azure Login" -echo "-------------------" -az login - -# Get subscription ID -SUBSCRIPTION_ID=$(az account show --query id --output tsv) -echo "โœ… Using subscription: $SUBSCRIPTION_ID" - -# Create Resource Group -echo "" -echo "Step 2: Create Resource Group" -echo "-----------------------------" -if az group show --name $RESOURCE_GROUP >/dev/null 2>&1; then - echo "โœ… Resource group $RESOURCE_GROUP already exists" -else - az group create --name $RESOURCE_GROUP --location $LOCATION - echo "โœ… Created resource group: $RESOURCE_GROUP" -fi - -# Create shared Application Insights -echo "" -echo "Step 3: Create Shared Application Insights" -echo "------------------------------------------" -cd "$(dirname "$0")" -az deployment group create \ - --resource-group $RESOURCE_GROUP \ - --template-file ../.azure/app-insights.bicep \ - --parameters location=$LOCATION -echo "โœ… Application Insights created" - -# Create Key Vault -echo "" -echo "Step 4: Create Azure Key Vault" -echo "------------------------------" -if az keyvault show --name $KEYVAULT_NAME >/dev/null 2>&1; then - echo "โœ… Key Vault $KEYVAULT_NAME already exists" -else - az keyvault create \ - --name $KEYVAULT_NAME \ - --resource-group $RESOURCE_GROUP \ - --location $LOCATION \ - --enable-rbac-authorization false - echo "โœ… Created Key Vault: $KEYVAULT_NAME" -fi - -# Add secrets to Key Vault -echo "" -echo "Step 5: Add Secrets to Key Vault" -echo "--------------------------------" -echo "IMPORTANT: Hyperliquid Wallet Architecture:" -echo " - Agent Wallet: API credentials (address + private key) for signing transactions" -echo " - Vault Address: The actual funded account where your funds are deposited" -echo "" -echo "You need BOTH for each environment. We'll store these as SECRETS (not Keys)." -echo "We'll check which secrets already exist and only add missing ones." -echo "" - -# Helper function to check if secret exists -secret_exists() { - az keyvault secret show --vault-name $KEYVAULT_NAME --name "$1" >/dev/null 2>&1 +usage() { + echo "Usage: $0 [location]" + echo "Creates the non-secret Azure foundation for exactly one environment." } -# Development (testnet) secrets -echo "Development Environment (Testnet):" -echo "-----------------------------------" -if secret_exists "hyperliquid-dev-agent-address" && secret_exists "hyperliquid-dev-agent-privatekey"; then - echo "โœ… Development agent wallet secrets already exist (skipping)" -else - echo "Agent/API Wallet (for signing transactions):" - read -p " Enter agent wallet address: " DEV_AGENT_ADDRESS - read -sp " Enter agent wallet private key: " DEV_AGENT_KEY - echo "" - - az keyvault secret set --vault-name $KEYVAULT_NAME --name "hyperliquid-dev-agent-address" --value "$DEV_AGENT_ADDRESS" >/dev/null - az keyvault secret set --vault-name $KEYVAULT_NAME --name "hyperliquid-dev-agent-privatekey" --value "$DEV_AGENT_KEY" >/dev/null - echo "โœ… Development agent wallet secrets added" +if [[ $# -lt 3 || $# -gt 4 ]]; then + usage + exit 1 fi -# Strategy-specific vault addresses (dev) -echo "" -echo "Strategy Vault Addresses (the funded accounts to trade with):" -echo " You can add multiple strategies. Press Enter with empty name when done." -echo "" +ENVIRONMENT="$1" +RESOURCE_GROUP="$2" +KEYVAULT_NAME="$3" +LOCATION="${4:-switzerlandnorth}" -STRATEGY_NUM=1 -while true; do - read -p " Strategy $STRATEGY_NUM name (e.g., 'momentumdaily', or Enter to skip): " STRATEGY_NAME - - if [ -z "$STRATEGY_NAME" ]; then - break - fi - - SECRET_NAME="hyperliquid-dev-vault-${STRATEGY_NAME}" - - if secret_exists "$SECRET_NAME"; then - echo " โœ… Vault address for $STRATEGY_NAME already exists (skipping)" - else - read -p " Enter vault address for $STRATEGY_NAME: " VAULT_ADDRESS - az keyvault secret set --vault-name $KEYVAULT_NAME --name "$SECRET_NAME" --value "$VAULT_ADDRESS" >/dev/null - echo " โœ… Vault address for $STRATEGY_NAME added" - fi - - STRATEGY_NUM=$((STRATEGY_NUM + 1)) - echo "" -done - -# Production (mainnet) secrets -echo "" -echo "Production Environment (Mainnet):" -echo "----------------------------------" -if secret_exists "hyperliquid-prod-agent-address" && secret_exists "hyperliquid-prod-agent-privatekey"; then - echo "โœ… Production agent wallet secrets already exist (skipping)" -else - echo "Agent/API Wallet (for signing transactions):" - read -p " Enter agent wallet address: " PROD_AGENT_ADDRESS - read -sp " Enter agent wallet private key: " PROD_AGENT_KEY - echo "" - - az keyvault secret set --vault-name $KEYVAULT_NAME --name "hyperliquid-prod-agent-address" --value "$PROD_AGENT_ADDRESS" >/dev/null - az keyvault secret set --vault-name $KEYVAULT_NAME --name "hyperliquid-prod-agent-privatekey" --value "$PROD_AGENT_KEY" >/dev/null - echo "โœ… Production agent wallet secrets added" +if [[ "$ENVIRONMENT" != "dev" && "$ENVIRONMENT" != "prod" ]]; then + usage + exit 1 fi -# Strategy-specific vault addresses (prod) -echo "" -echo "Strategy Vault Addresses (the funded accounts to trade with):" -echo " You can add multiple strategies. Press Enter with empty name when done." -echo "" - -STRATEGY_NUM=1 -while true; do - read -p " Strategy $STRATEGY_NUM name (e.g., 'momentumdaily', or Enter to skip): " STRATEGY_NAME - - if [ -z "$STRATEGY_NAME" ]; then - break - fi - - SECRET_NAME="hyperliquid-prod-vault-${STRATEGY_NAME}" - - if secret_exists "$SECRET_NAME"; then - echo " โœ… Vault address for $STRATEGY_NAME already exists (skipping)" - else - read -p " Enter vault address for $STRATEGY_NAME: " VAULT_ADDRESS - az keyvault secret set --vault-name $KEYVAULT_NAME --name "$SECRET_NAME" --value "$VAULT_ADDRESS" >/dev/null - echo " โœ… Vault address for $STRATEGY_NAME added" - fi - - STRATEGY_NUM=$((STRATEGY_NUM + 1)) - echo "" -done - -# RobotWealth API key -echo "" -if secret_exists "robotwealth-api-key"; then - echo "โœ… RobotWealth API key already exists (skipping)" -else - read -sp "Enter RobotWealth API key: " ROBOTWEALTH_KEY - echo "" - az keyvault secret set --vault-name $KEYVAULT_NAME --name "robotwealth-api-key" --value "$ROBOTWEALTH_KEY" >/dev/null - echo "โœ… RobotWealth API key added" -fi - -# Create Service Principal -echo "" -echo "Step 6: Create Service Principal for GitHub" -echo "-------------------------------------------" -SP_JSON=$(az ad sp create-for-rbac \ - --name $SP_NAME \ - --role contributor \ - --scopes /subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP \ - --sdk-auth) - -echo "โœ… Service Principal created" - -# Allow GitHub Actions to grant and remove Key Vault RBAC assignments for Function App managed identities. -# The workflow itself grants only Key Vault Secrets User to each Function App identity. -KEYVAULT_ID=$(az keyvault show --name $KEYVAULT_NAME --query id --output tsv) -SP_OBJECT_ID=$(az ad sp list --display-name "$SP_NAME" --query "[0].id" --output tsv) - -if [ -z "$SP_OBJECT_ID" ]; then - echo "โŒ Could not resolve service principal object id for $SP_NAME" - exit 1 -fi - -EXISTING_UAA_ASSIGNMENT=$(az role assignment list \ - --assignee "$SP_OBJECT_ID" \ - --role "User Access Administrator" \ - --scope "$KEYVAULT_ID" \ - --query "[0].id" \ - --output tsv) - -if [ -z "$EXISTING_UAA_ASSIGNMENT" ]; then - az role assignment create \ - --assignee-object-id "$SP_OBJECT_ID" \ - --assignee-principal-type ServicePrincipal \ - --role "User Access Administrator" \ - --scope "$KEYVAULT_ID" \ - >/dev/null -fi +command -v az >/dev/null 2>&1 || { + echo "Azure CLI is required: https://aka.ms/InstallAzureCLI" + exit 1 +} -echo "โœ… Service Principal can manage Key Vault role assignments" +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +REPO_ROOT=$(cd "$SCRIPT_DIR/.." && pwd) -# Save to file -mkdir -p .github/secrets -echo "$SP_JSON" > .github/secrets/AZURE_CREDENTIALS.json -echo "โœ… Saved credentials to .github/secrets/AZURE_CREDENTIALS.json" +az account show >/dev/null 2>&1 || az login -# Add to GitHub -echo "" -echo "Step 7: Configure GitHub Secrets" -echo "--------------------------------" +echo "Provisioning $ENVIRONMENT foundation in $RESOURCE_GROUP ($LOCATION)" +az group create \ + --name "$RESOURCE_GROUP" \ + --location "$LOCATION" \ + --tags Environment="$ENVIRONMENT" ManagedBy=GitHub -if [ "$GH_INSTALLED" != false ]; then - echo "Adding secrets to GitHub repository..." - - # Check if we're in a GitHub repo - if gh repo view >/dev/null 2>&1; then - gh secret set AZURE_CREDENTIALS < .github/secrets/AZURE_CREDENTIALS.json - echo "โœ… AZURE_CREDENTIALS added to GitHub" - - gh variable set AZURE_KEYVAULT_NAME --body "$KEYVAULT_NAME" - echo "โœ… AZURE_KEYVAULT_NAME variable added to GitHub" - else - echo "โš ๏ธ Not in a GitHub repository. Skipping GitHub secret configuration." - echo " You'll need to manually add AZURE_CREDENTIALS to GitHub secrets." - fi -else - echo "โš ๏ธ GitHub CLI not installed." - echo " Manually add the following secret to your GitHub repository:" - echo " Name: AZURE_CREDENTIALS" - echo " Value: (contents of .github/secrets/AZURE_CREDENTIALS.json)" - echo "" - echo " Also add this variable:" - echo " Name: AZURE_KEYVAULT_NAME" - echo " Value: $KEYVAULT_NAME" -fi - -# Create GitHub Environments -echo "" -echo "Step 8: Create GitHub Environments" -echo "----------------------------------" -echo "โš ๏ธ Manual step required:" -echo " 1. Go to your GitHub repository โ†’ Settings โ†’ Environments" -echo " 2. Create 'production' environment" -echo " 3. Enable 'Required reviewers' and add yourself" -echo " 4. (Optional) Create 'development' environment" +az deployment group create \ + --resource-group "$RESOURCE_GROUP" \ + --template-file "$REPO_ROOT/.azure/key-vault.bicep" \ + --parameters \ + keyVaultName="$KEYVAULT_NAME" \ + environmentName="$ENVIRONMENT" \ + location="$LOCATION" -# Create develop branch -echo "" -echo "Step 9: Create develop branch" -echo "-----------------------------" -if git rev-parse --verify develop >/dev/null 2>&1; then - echo "โœ… develop branch already exists" -else - read -p "Create and push develop branch? (y/n) " -n 1 -r - echo - if [[ $REPLY =~ ^[Yy]$ ]]; then - git checkout -b develop - git push -u origin develop - git checkout - - echo "โœ… develop branch created and pushed" - else - echo "โš ๏ธ Skipped creating develop branch" - fi -fi +az deployment group create \ + --resource-group "$RESOURCE_GROUP" \ + --template-file "$REPO_ROOT/.azure/app-insights.bicep" \ + --parameters environmentName="$ENVIRONMENT" location="$LOCATION" -# Summary -echo "" -echo "========================================" -echo "โœ… Setup Complete!" -echo "========================================" -echo "" -echo "Next steps:" -echo " 1. Complete GitHub Environment setup (see Step 8 above)" -echo " 2. Push code to trigger first deployment:" -echo " - Push to 'develop' โ†’ deploys to yolo-funk-dev" -echo " - Push to 'master' โ†’ deploys to yolo-funk-prod (requires approval)" -echo " - Create PR โ†’ deploys to ephemeral environment" -echo "" -echo " 3. Configure Function App settings after first deployment:" -echo " See DEPLOYMENT.md for configuration examples" -echo "" -echo "Resources created:" -echo " - Resource Group: $RESOURCE_GROUP" -echo " - Application Insights: yolo-funk-insights" -echo " - Key Vault: $KEYVAULT_NAME" -echo " - Service Principal: $SP_NAME" -echo "" -echo "Security note:" -echo " - AZURE_CREDENTIALS saved to .github/secrets/" -echo " - This directory is in .gitignore - do NOT commit!" -echo " - Delete after confirming GitHub secrets are configured" -echo "" +echo +echo "Foundation created. Populate $KEYVAULT_NAME using:" +echo " docs/AZURE-KEY-VAULT-SECRETS-SETUP.md" +echo +echo "Configure the GitHub $([[ "$ENVIRONMENT" == "prod" ]] && echo production || echo development) environment with:" +echo " AZURE_RESOURCE_GROUP=$RESOURCE_GROUP" +echo " AZURE_LOCATION=$LOCATION" +echo " AZURE_KEYVAULT_NAME=$KEYVAULT_NAME" +echo "and an environment-scoped AZURE_CREDENTIALS secret."