Skip to content

Deploy Staging

Deploy Staging #453

Workflow file for this run

name: Deploy Staging
permissions:
contents: read
deployments: write
id-token: write
security-events: write # Required for Trivy SARIF upload in test workflow
concurrency:
group: robosystems-deploy
cancel-in-progress: false
on:
workflow_dispatch:
inputs:
graph_container_refresh:
description: "Refresh graph containers via SSM after deploy"
required: false
type: boolean
default: true
graph_refresh_max_wait_minutes:
description: "Minutes to wait for in-flight destructive ops (materialization, SEC stage, ext mat) before cycling"
required: false
type: string
default: "30"
graph_refresh_force_ignore_busy:
description: "Skip busy-counter check and cycle graph containers immediately (emergency override)"
required: false
type: boolean
default: false
graph_writer_az:
description: "Move ALL LadybugDB writer tiers to this AZ (e.g. us-east-1d). Recovery/capacity only — replaces every writer instance onto an empty volume. Leave empty for normal deploys; after a move, set GRAPH_WRITER_AZ_STAGING to the same value so later releases stay there."
required: false
type: string
default: ""
jobs:
# Guard: Verify staging environment is enabled
check-staging-enabled:
runs-on: ubuntu-latest
steps:
- name: Check if staging is enabled
run: |
if [ "${{ vars.ENVIRONMENT_STAGING }}" != "staging" ]; then
echo "❌ Staging environment is not enabled"
echo ""
echo "To enable staging deployments, set the GitHub variable:"
echo " gh variable set ENVIRONMENT_STAGING --body staging"
echo ""
echo "Or run: just setup-gha (select 'Production + Staging')"
exit 1
fi
echo "✅ Staging environment is enabled"
runner:
needs: [check-staging-enabled]
runs-on: ubuntu-latest
outputs:
runners_available: ${{ steps.select.outputs.runners_available }}
runner_type: ${{ steps.select.outputs.runner_type }}
runner_config: ${{ steps.select.outputs.runner_config }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
repository: ${{ github.repository }}
ref: ${{ github.ref }}
token: ${{ github.token }}
- name: Select runner
id: select
uses: ./.github/actions/select-runner
with:
runner_labels: ${{ vars.RUNNER_LABELS || 'github-hosted' }}
runner_scope: ${{ vars.RUNNER_SCOPE || 'both' }}
github_token: ${{ github.token }}
test:
needs: [runner]
uses: ./.github/workflows/test.yml
with:
runner_config: ${{ needs.runner.outputs.runner_config }}
secrets: inherit
stack-config:
needs: [runner]
runs-on: ${{ fromJSON(needs.runner.outputs.runner_config) }}
permissions:
id-token: write
contents: read
outputs:
vpc_stack: ${{ steps.load.outputs.vpc_stack }}
s3_stack: ${{ steps.load.outputs.s3_stack }}
postgres_stack: ${{ steps.load.outputs.postgres_stack }}
valkey_stack: ${{ steps.load.outputs.valkey_stack }}
api_stack: ${{ steps.load.outputs.api_stack }}
graph_infra_stack: ${{ steps.load.outputs.graph_infra_stack }}
graph_volumes_stack: ${{ steps.load.outputs.graph_volumes_stack }}
prometheus_stack: ${{ steps.load.outputs.prometheus_stack }}
grafana_stack: ${{ steps.load.outputs.grafana_stack }}
bastion_stack: ${{ steps.load.outputs.bastion_stack }}
waf_stack: ${{ steps.load.outputs.waf_stack }}
audit_stack: ${{ steps.load.outputs.audit_stack }}
cloudtrail_stack: ${{ steps.load.outputs.cloudtrail_stack }}
security_stack: ${{ steps.load.outputs.security_stack }}
dagster_stack: ${{ steps.load.outputs.dagster_stack }}
worker_stack: ${{ steps.load.outputs.worker_stack }}
opensearch_stack: ${{ steps.load.outputs.opensearch_stack }}
domain_name_root: ${{ steps.load.outputs.domain_name_root }}
graph_ami_id: ${{ steps.ami.outputs.ami_id }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
repository: ${{ github.repository }}
ref: ${{ github.ref }}
token: ${{ github.token }}
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6
with:
role-to-assume: ${{ vars.AWS_ROLE_ARN }}
aws-region: ${{ vars.AWS_REGION || 'us-east-1' }}
- name: Get Graph AMI ID
id: ami
uses: ./.github/actions/get-graph-ami
with:
environment: staging
- name: Install PyYAML
shell: bash
run: pip install --user pyyaml
- name: Load Stack Configuration
id: load
shell: bash
run: |
chmod +x bin/tools/stack-config.sh
ENV="staging"
# Load all stack names from config
echo "vpc_stack=$(./bin/tools/stack-config.sh $ENV vpc)" >> $GITHUB_OUTPUT
echo "s3_stack=$(./bin/tools/stack-config.sh $ENV s3)" >> $GITHUB_OUTPUT
echo "postgres_stack=$(./bin/tools/stack-config.sh $ENV postgres)" >> $GITHUB_OUTPUT
echo "valkey_stack=$(./bin/tools/stack-config.sh $ENV valkey)" >> $GITHUB_OUTPUT
echo "api_stack=$(./bin/tools/stack-config.sh $ENV api)" >> $GITHUB_OUTPUT
echo "graph_infra_stack=$(./bin/tools/stack-config.sh $ENV graph.infra)" >> $GITHUB_OUTPUT
echo "graph_volumes_stack=$(./bin/tools/stack-config.sh $ENV graph.volumes)" >> $GITHUB_OUTPUT
echo "prometheus_stack=$(./bin/tools/stack-config.sh $ENV monitoring.prometheus)" >> $GITHUB_OUTPUT
echo "grafana_stack=$(./bin/tools/stack-config.sh $ENV monitoring.grafana)" >> $GITHUB_OUTPUT
echo "bastion_stack=$(./bin/tools/stack-config.sh $ENV bastion)" >> $GITHUB_OUTPUT
echo "waf_stack=$(./bin/tools/stack-config.sh $ENV waf)" >> $GITHUB_OUTPUT
echo "audit_stack=$(./bin/tools/stack-config.sh $ENV audit)" >> $GITHUB_OUTPUT
echo "cloudtrail_stack=$(./bin/tools/stack-config.sh $ENV cloudtrail)" >> $GITHUB_OUTPUT
echo "security_stack=$(./bin/tools/stack-config.sh $ENV security)" >> $GITHUB_OUTPUT
echo "dagster_stack=$(./bin/tools/stack-config.sh $ENV dagster)" >> $GITHUB_OUTPUT
echo "worker_stack=$(./bin/tools/stack-config.sh $ENV workers)" >> $GITHUB_OUTPUT
echo "opensearch_stack=$(./bin/tools/stack-config.sh $ENV opensearch)" >> $GITHUB_OUTPUT
# Domain configuration - leave empty for isolated deployment (no custom domains)
echo "domain_name_root=${{ vars.API_DOMAIN_NAME_ROOT || '' }}" >> $GITHUB_OUTPUT
build:
needs: [runner]
uses: ./.github/workflows/build.yml
with:
environment: ${{ vars.ENVIRONMENT_STAGING || 'staging' }}
aws_region: ${{ vars.AWS_REGION || 'us-east-1' }}
ecr_repository: ${{ vars.AWS_ECR_REPOSITORY || 'robosystems' }}
# Docker Hub publishing for staging: disabled (staging builds should not go to Docker Hub)
publish_to_dockerhub: false
secrets:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
create-deployment:
needs: [runner, test, build, stack-config]
runs-on: ${{ fromJSON(needs.runner.outputs.runner_config) }}
outputs:
deployment_id: ${{ steps.deployment.outputs.deployment_id }}
steps:
- name: Create GitHub Deployment
id: deployment
uses: chrnorm/deployment-action@500aa6a23c81ffa1acf71072aee3cfa2cc2e556a # v2
with:
token: ${{ github.token }}
environment: ${{ vars.ENVIRONMENT_STAGING }}
description: "Staging Deployment Created"
- name: Update Deployment Status
uses: chrnorm/deployment-status@6df8d036fd2fee9eb82936733953da1f8382b41e # v2
with:
token: ${{ github.token }}
state: in_progress
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
description: "Staging Deployment In Progress"
deploy-vpc:
needs: [runner, create-deployment, stack-config]
uses: ./.github/workflows/deploy-vpc.yml
with:
# Stack & Repository Configuration
stack_name: ${{ needs.stack-config.outputs.vpc_stack }}
cloudtrail_stack_name: ${{ needs.stack-config.outputs.cloudtrail_stack }}
environment: shared
# GHA Runner Configuration
runner_config: ${{ needs.runner.outputs.runner_config }}
# AWS Configuration
aws_region: ${{ vars.AWS_REGION || 'us-east-1' }}
# VPC Configuration
endpoint_mode: ${{ vars.VPC_ENDPOINT_MODE || 'none' }}
max_availability_zones: ${{ vars.VPC_MAX_AVAILABILITY_ZONES || '2' }}
vpc_second_octet: ${{ vars.VPC_SECOND_OCTET || '0' }}
# VPC Flow Logs Configuration (VPC-level, not environment-specific)
vpc_flow_logs_enabled: ${{ vars.VPC_FLOW_LOGS_ENABLED || 'false' }}
vpc_flow_logs_retention_days: ${{ vars.VPC_FLOW_LOGS_RETENTION_DAYS || '30' }}
vpc_flow_logs_traffic_type: ${{ vars.VPC_FLOW_LOGS_TRAFFIC_TYPE || 'REJECT' }}
# CloudTrail Configuration (account-level, not environment-specific)
cloudtrail_enabled: ${{ vars.CLOUDTRAIL_ENABLED || 'false' }}
cloudtrail_log_retention_days: ${{ vars.CLOUDTRAIL_LOG_RETENTION_DAYS || '90' }}
cloudtrail_data_events_enabled: ${{ vars.CLOUDTRAIL_DATA_EVENTS_ENABLED || 'false' }}
# Security Baseline Configuration (account-level, not environment-specific)
security_enabled: ${{ vars.SECURITY_ENABLED || 'false' }}
security_stack_name: ${{ needs.stack-config.outputs.security_stack }}
security_config_enabled: ${{ vars.SECURITY_CONFIG_ENABLED || 'false' }}
secrets: inherit
deploy-s3:
needs: [runner, create-deployment, stack-config]
uses: ./.github/workflows/deploy-s3.yml
with:
# Stack & Repository Configuration
stack_name: ${{ needs.stack-config.outputs.s3_stack }}
environment: ${{ vars.ENVIRONMENT_STAGING || 'staging' }}
# GHA Runner Configuration
runner_config: ${{ needs.runner.outputs.runner_config }}
# AWS Configuration
aws_region: ${{ vars.AWS_REGION || 'us-east-1' }}
# Namespace Configuration (always use account ID for globally unique bucket names)
namespace: ${{ vars.AWS_ACCOUNT_ID }}
# Domain Configuration
# Leave PUBLIC_DOMAIN_NAME_STAGING empty for default CloudFront domain (isolated deployment)
public_domain_name: ${{ vars.PUBLIC_DOMAIN_NAME_STAGING || '' }}
domain_name_root: ${{ vars.API_DOMAIN_NAME_ROOT || '' }}
secrets: inherit
package-scripts:
needs: [runner, deploy-s3]
runs-on: ${{ fromJSON(needs.runner.outputs.runner_config) }}
permissions:
id-token: write
contents: read
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
repository: ${{ github.repository }}
ref: ${{ github.ref }}
token: ${{ github.token }}
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6
with:
role-to-assume: ${{ vars.AWS_ROLE_ARN }}
aws-region: ${{ vars.AWS_REGION || 'us-east-1' }}
- name: Package and upload scripts
run: |
echo "📦 Packaging UserData scripts..."
./bin/tools/package-scripts.sh ${{ vars.ENVIRONMENT_STAGING || 'staging' }}
echo "✅ Scripts packaged and uploaded successfully"
deploy-postgres:
needs: [runner, create-deployment, stack-config, build, deploy-vpc]
uses: ./.github/workflows/deploy-postgres.yml
with:
# Stack & Repository Configuration
stack_name: ${{ needs.stack-config.outputs.postgres_stack }}
environment: ${{ vars.ENVIRONMENT_STAGING || 'staging' }}
# GHA Runner Configuration
runner_config: ${{ needs.runner.outputs.runner_config }}
# AWS Configuration
aws_region: ${{ vars.AWS_REGION || 'us-east-1' }}
vpc_id: ${{ needs.deploy-vpc.outputs.vpc_id }}
vpc_cidr: ${{ needs.deploy-vpc.outputs.vpc_cidr }}
subnet_ids: ${{ needs.deploy-vpc.outputs.private_subnet_ids }}
# Database Version Configuration
postgres_version: ${{ vars.DATABASE_POSTGRES_VERSION_STAGING || '16.11' }}
# Instance Configuration
instance_size: ${{ vars.DATABASE_INSTANCE_SIZE_STAGING || 'db.t4g.small' }}
allocated_storage: ${{ vars.DATABASE_ALLOCATED_STORAGE_STAGING || '20' }}
allocated_max_storage: ${{ vars.DATABASE_MAX_ALLOCATED_STORAGE_STAGING || '100' }}
# High Availability Configuration
multi_az_enabled: ${{ vars.DATABASE_MULTI_AZ_ENABLED_STAGING || 'false' }}
# RDS Proxy Configuration (off by default - flip RDS_PROXY_ENABLED_STAGING to 'true' to turn on)
rds_proxy_enabled: ${{ vars.RDS_PROXY_ENABLED_STAGING || 'false' }}
rds_proxy_max_connections_percent: ${{ vars.RDS_PROXY_MAX_CONNECTIONS_PERCENT_STAGING || '100' }}
rds_proxy_connection_borrow_timeout: ${{ vars.RDS_PROXY_CONNECTION_BORROW_TIMEOUT_STAGING || '120' }}
# Performance Insights (Database Insights Standard mode - off by default in staging)
performance_insights_enabled: ${{ vars.DATABASE_PI_ENABLED_STAGING || 'false' }}
performance_insights_retention_days: ${{ vars.DATABASE_PI_RETENTION_DAYS_STAGING || '7' }}
# Lambda Configuration (container-based)
lambda_image_uri: ${{ needs.build.outputs.lambda_image }}
# Other Configuration
aws_sns_alert_email: ${{ vars.AWS_SNS_ALERT_EMAIL }}
secrets: inherit
deploy-valkey:
needs: [runner, create-deployment, stack-config, build, deploy-vpc]
uses: ./.github/workflows/deploy-valkey.yml
with:
# Stack & Repository Configuration
stack_name: ${{ needs.stack-config.outputs.valkey_stack }}
environment: ${{ vars.ENVIRONMENT_STAGING || 'staging' }}
# GHA Runner Configuration
runner_config: ${{ needs.runner.outputs.runner_config }}
# AWS Configuration
aws_region: ${{ vars.AWS_REGION || 'us-east-1' }}
vpc_id: ${{ needs.deploy-vpc.outputs.vpc_id }}
subnet_ids: ${{ needs.deploy-vpc.outputs.private_subnet_ids }}
# Instance Configuration
node_type: ${{ vars.VALKEY_NODE_TYPE_STAGING || 'cache.t4g.micro' }}
num_cache_nodes: ${{ vars.VALKEY_NUM_NODES_STAGING || '1' }}
# Valkey Version Configuration
valkey_version: ${{ vars.VALKEY_VERSION_STAGING || '8.1' }}
# Security Configuration
encryption_enabled: ${{ vars.VALKEY_ENCRYPTION_ENABLED_STAGING || 'true' }}
# Backup Configuration
snapshot_retention_days: ${{ vars.VALKEY_SNAPSHOT_RETENTION_DAYS_STAGING || '0' }}
# Lambda Configuration (container-based)
lambda_image_uri: ${{ needs.build.outputs.lambda_image }}
# Other Configuration
aws_sns_alert_email: ${{ vars.AWS_SNS_ALERT_EMAIL }}
secrets: inherit
deploy-opensearch:
# Text search disabled by default - set OPENSEARCH_ENABLED_STAGING=true to enable
if: vars.OPENSEARCH_ENABLED_STAGING == 'true'
needs: [runner, create-deployment, stack-config, deploy-vpc, deploy-bastion]
uses: ./.github/workflows/deploy-opensearch.yml
with:
# Stack & Repository Configuration
stack_name: ${{ needs.stack-config.outputs.opensearch_stack }}
environment: ${{ vars.ENVIRONMENT_STAGING || 'staging' }}
# GHA Runner Configuration
runner_config: ${{ needs.runner.outputs.runner_config }}
# AWS Configuration
aws_region: ${{ vars.AWS_REGION || 'us-east-1' }}
vpc_id: ${{ needs.deploy-vpc.outputs.vpc_id }}
subnet_ids: ${{ needs.deploy-vpc.outputs.private_subnet_ids }}
# Instance Configuration
instance_type: ${{ vars.OPENSEARCH_INSTANCE_TYPE_STAGING || 't3.medium.search' }}
instance_count: ${{ vars.OPENSEARCH_INSTANCE_COUNT_STAGING || '1' }}
zone_awareness_enabled: ${{ vars.OPENSEARCH_ZONE_AWARENESS_STAGING || 'false' }}
availability_zone_count: ${{ vars.OPENSEARCH_AZ_COUNT_STAGING || '2' }}
dedicated_master_enabled: ${{ vars.OPENSEARCH_DEDICATED_MASTER_STAGING || 'false' }}
dedicated_master_type: ${{ vars.OPENSEARCH_MASTER_TYPE_STAGING || 't3.small.search' }}
dedicated_master_count: ${{ vars.OPENSEARCH_MASTER_COUNT_STAGING || '3' }}
ebs_volume_size: ${{ vars.OPENSEARCH_EBS_SIZE_STAGING || '100' }}
# Engine Version Configuration
engine_version: ${{ vars.OPENSEARCH_VERSION_STAGING || '2.19' }}
# Bastion Access
bastion_sg_id: ${{ needs.deploy-bastion.outputs.bastion_sg_id }}
# Other Configuration
aws_sns_alert_email: ${{ vars.AWS_SNS_ALERT_EMAIL }}
secrets: inherit
deploy-prometheus:
# Observability disabled by default - set OBSERVABILITY_ENABLED_STAGING=true to enable
if: vars.OBSERVABILITY_ENABLED_STAGING == 'true'
needs: [runner, create-deployment, stack-config, deploy-vpc]
uses: ./.github/workflows/deploy-prometheus.yml
with:
# Stack & Repository Configuration
stack_name: ${{ needs.stack-config.outputs.prometheus_stack }}
environment: ${{ vars.ENVIRONMENT_STAGING || 'staging' }}
# GHA Runner Configuration
runner_config: ${{ needs.runner.outputs.runner_config }}
# AWS Configuration
aws_region: ${{ vars.AWS_REGION || 'us-east-1' }}
secrets: inherit
deploy-grafana:
# Observability disabled by default - set OBSERVABILITY_ENABLED_STAGING=true to enable
if: vars.OBSERVABILITY_ENABLED_STAGING == 'true'
needs: [runner, create-deployment, stack-config]
uses: ./.github/workflows/deploy-grafana.yml
with:
# Stack & Repository Configuration
stack_name: ${{ needs.stack-config.outputs.grafana_stack }}
environment: shared
# GHA Runner Configuration
runner_config: ${{ needs.runner.outputs.runner_config }}
# AWS Configuration
aws_region: ${{ vars.AWS_REGION || 'us-east-1' }}
secrets: inherit
deploy-bastion:
needs:
[
runner,
create-deployment,
stack-config,
deploy-vpc,
deploy-postgres,
deploy-valkey,
deploy-s3,
]
uses: ./.github/workflows/deploy-bastion.yml
with:
# Stack & Repository Configuration
stack_name: ${{ needs.stack-config.outputs.bastion_stack }}
environment: ${{ vars.ENVIRONMENT_STAGING || 'staging' }}
# GHA Runner Configuration
runner_config: ${{ needs.runner.outputs.runner_config }}
# AWS Configuration
aws_region: ${{ vars.AWS_REGION || 'us-east-1' }}
vpc_id: ${{ needs.deploy-vpc.outputs.vpc_id }}
public_subnet_ids: ${{ needs.deploy-vpc.outputs.public_subnet_ids }}
# Security Groups (SSM-only access - no SSH parameters needed)
valkey_sg_id: ${{ needs.deploy-valkey.outputs.valkey_sg_id }}
# S3 Bucket Configuration (from S3 stack outputs)
deployment_bucket_arn: ${{ needs.deploy-s3.outputs.deployment_bucket_arn }}
secrets: inherit
# Graph Infrastructure - Step 1: Deploy DynamoDB, Secrets, Lambda monitoring
deploy-graph-infra:
needs:
[runner, create-deployment, stack-config, build, deploy-vpc, deploy-s3]
uses: ./.github/workflows/deploy-graph-infra.yml
with:
# Stack & Repository Configuration
stack_name: ${{ needs.stack-config.outputs.graph_infra_stack }}
environment: ${{ vars.ENVIRONMENT_STAGING || 'staging' }}
# GHA Runner Configuration
runner_config: ${{ needs.runner.outputs.runner_config }}
# AWS Configuration
aws_region: ${{ vars.AWS_REGION || 'us-east-1' }}
# Lambda Configuration (container-based)
lambda_image_uri: ${{ needs.build.outputs.lambda_image }}
# Other Configuration
aws_sns_alert_email: ${{ vars.AWS_SNS_ALERT_EMAIL }}
secrets: inherit
# Graph Infrastructure - Step 2: Deploy EBS volume management
deploy-graph-volumes:
needs: [runner, stack-config, build, deploy-vpc, deploy-graph-infra]
uses: ./.github/workflows/deploy-graph-volumes.yml
with:
# Stack & Repository Configuration
stack_name: ${{ needs.stack-config.outputs.graph_volumes_stack }}
environment: ${{ vars.ENVIRONMENT_STAGING || 'staging' }}
# GHA Runner Configuration
runner_config: ${{ needs.runner.outputs.runner_config }}
# AWS Configuration
aws_region: ${{ vars.AWS_REGION || 'us-east-1' }}
vpc_id: ${{ needs.deploy-vpc.outputs.vpc_id }}
subnet_ids: ${{ needs.deploy-vpc.outputs.private_subnet_ids }}
# Registry Configuration
volume_registry_table: ${{ needs.deploy-graph-infra.outputs.volume_registry_table }}
# Lambda Configuration (container-based)
lambda_image_uri: ${{ needs.build.outputs.lambda_image }}
# Secrets Configuration
graph_api_secret_arn: ${{ needs.deploy-graph-infra.outputs.secret_arn }}
# Other Configuration
aws_sns_alert_email: ${{ vars.AWS_SNS_ALERT_EMAIL }}
secrets: inherit
deploy-graph:
needs:
[
runner,
create-deployment,
stack-config,
build,
deploy-vpc,
deploy-postgres,
deploy-valkey,
deploy-s3,
deploy-graph-infra,
deploy-graph-volumes,
package-scripts,
]
uses: ./.github/workflows/deploy-graph.yml
with:
# Environment Configuration
environment: ${{ vars.ENVIRONMENT_STAGING || 'staging' }}
# GHA Runner Configuration
runner_config: ${{ needs.runner.outputs.runner_config }}
# AWS Configuration
aws_region: ${{ vars.AWS_REGION || 'us-east-1' }}
vpc_id: ${{ needs.deploy-vpc.outputs.vpc_id }}
subnet_ids: ${{ needs.deploy-vpc.outputs.private_subnet_ids }}
public_subnet_ids: ${{ needs.deploy-vpc.outputs.public_subnet_ids }}
valkey_sg_id: ${{ needs.deploy-valkey.outputs.valkey_sg_id }}
valkey_url: ${{ needs.deploy-valkey.outputs.valkey_url }}
database_endpoint: ${{ needs.deploy-postgres.outputs.database_endpoint }}
database_port: ${{ needs.deploy-postgres.outputs.database_port }}
ami_id: ${{ needs.stack-config.outputs.graph_ami_id }}
# Container Configuration - use staging tag for EC2/ASG (container refresh handles updates)
ecr_image_tag: staging
# SEC shared replicas track the moving staging tag by default (no per-deploy
# ECRImageTag change → CloudFormation doesn't cycle the fleet; the daily
# shared_replicas_refresh is the only routine boot). Leave
# shared_replica_image_tag unset for that. Pin it ONLY during a
# storage-format-breaking engine upgrade, so a boot can't pull a new engine
# before the new-format sec.lbug is published — set a
# SHARED_REPLICA_IMAGE_TAG_STAGING var for the migration window, then clear it.
shared_replica_image_tag: ${{ vars.SHARED_REPLICA_IMAGE_TAG_STAGING || '' }}
# Infrastructure inputs from graph infrastructure jobs
secret_arn: ${{ needs.deploy-graph-infra.outputs.secret_arn }}
volume_registry_table: ${{ needs.deploy-graph-infra.outputs.volume_registry_table }}
volume_manager_function_arn: ${{ needs.deploy-graph-volumes.outputs.volume_manager_function_arn }}
volume_detachment_topic_arn: ${{ needs.deploy-graph-volumes.outputs.volume_detachment_topic_arn }}
# Writer placement. Every writer tier sits in ONE AZ (EBS volume affinity).
# The graph_writer_az dispatch input MOVES the tiers — a deliberate, per-run
# act. The GRAPH_WRITER_AZ_STAGING var SUSTAINS the AZ across the input-less
# dispatches from create-release / graph-maintenance and may only name the
# AZ the tiers already occupy (a mismatch fails the deploy). Both unset →
# <region>c, unchanged behaviour.
writer_az: ${{ inputs.graph_writer_az || vars.GRAPH_WRITER_AZ_STAGING || '' }}
writer_az_change_allowed: ${{ (inputs.graph_writer_az != '' && 'true') || 'false' }}
# Graph Tier Configuration
lbug_large_enabled: ${{ vars.LBUG_LARGE_ENABLED_STAGING || 'false' }}
lbug_xlarge_enabled: ${{ vars.LBUG_XLARGE_ENABLED_STAGING || 'false' }}
lbug_shared_enabled: ${{ vars.LBUG_SHARED_ENABLED_STAGING || 'false' }}
# Graph Scaling Configuration
lbug_standard_min_instances: ${{ vars.LBUG_STANDARD_MIN_INSTANCES_STAGING }}
lbug_standard_max_instances: ${{ vars.LBUG_STANDARD_MAX_INSTANCES_STAGING }}
lbug_large_min_instances: ${{ vars.LBUG_LARGE_MIN_INSTANCES_STAGING }}
lbug_large_max_instances: ${{ vars.LBUG_LARGE_MAX_INSTANCES_STAGING }}
lbug_xlarge_min_instances: ${{ vars.LBUG_XLARGE_MIN_INSTANCES_STAGING }}
lbug_xlarge_max_instances: ${{ vars.LBUG_XLARGE_MAX_INSTANCES_STAGING }}
lbug_shared_min_instances: ${{ vars.LBUG_SHARED_MIN_INSTANCES_STAGING }}
lbug_shared_max_instances: ${{ vars.LBUG_SHARED_MAX_INSTANCES_STAGING }}
# Shared Replicas Configuration
shared_replicas_enabled: ${{ vars.SHARED_REPLICAS_ENABLED_STAGING || 'false' }}
shared_replicas_min_instances: ${{ vars.SHARED_REPLICAS_MIN_INSTANCES_STAGING || '1' }}
shared_replicas_max_instances: ${{ vars.SHARED_REPLICAS_MAX_INSTANCES_STAGING || '3' }}
shared_replicas_desired_capacity: ${{ vars.SHARED_REPLICAS_DESIRED_CAPACITY_STAGING || '1' }}
shared_replicas_root_volume_size: ${{ vars.SHARED_REPLICAS_ROOT_VOLUME_SIZE_STAGING || '100' }}
shared_repositories: ${{ vars.SHARED_REPOSITORIES_STAGING || 'sec' }}
shared_replicas_cpu_target: ${{ vars.SHARED_REPLICAS_CPU_TARGET_STAGING || '70' }}
shared_replicas_memory_target: ${{ vars.SHARED_REPLICAS_MEMORY_TARGET_STAGING || '80' }}
shared_replicas_enable_response_time_scaling: ${{ vars.SHARED_REPLICAS_ENABLE_RESPONSE_TIME_SCALING_STAGING || 'false' }}
shared_replicas_response_time_target: ${{ vars.SHARED_REPLICAS_RESPONSE_TIME_TARGET_STAGING || '5' }}
shared_replicas_instance_warmup: ${{ vars.SHARED_REPLICAS_INSTANCE_WARMUP_STAGING || '900' }}
shared_replicas_health_check_grace_period: ${{ vars.SHARED_REPLICAS_HEALTH_CHECK_GRACE_PERIOD_STAGING || '900' }}
# Shared Replicas Spot Configuration
shared_replicas_spot_enabled: ${{ vars.SHARED_REPLICAS_SPOT_ENABLED_STAGING || 'false' }}
shared_replicas_od_base: ${{ vars.SHARED_REPLICAS_OD_BASE_STAGING || '0' }}
shared_replicas_spot_weight: ${{ vars.SHARED_REPLICAS_SPOT_WEIGHT_STAGING || '0' }}
# Other Configuration
aws_sns_alert_email: ${{ vars.AWS_SNS_ALERT_EMAIL }}
secrets: inherit
deploy-api:
needs:
[
runner,
create-deployment,
stack-config,
build,
deploy-vpc,
deploy-valkey,
deploy-postgres,
deploy-s3,
deploy-grafana,
deploy-prometheus,
deploy-opensearch,
deploy-bastion,
]
if: ${{ !cancelled() && !contains(needs.*.result, 'cancelled') && !contains(needs.*.result, 'failure') }}
uses: ./.github/workflows/deploy-api.yml
with:
# Stack & Repository Configuration
stack_name: ${{ needs.stack-config.outputs.api_stack }}
environment: ${{ vars.ENVIRONMENT_STAGING || 'staging' }}
# GHA Runner Configuration
runner_config: ${{ needs.runner.outputs.runner_config }}
# AWS Configuration
aws_region: ${{ vars.AWS_REGION || 'us-east-1' }}
vpc_id: ${{ needs.deploy-vpc.outputs.vpc_id }}
subnet_ids: ${{ needs.deploy-vpc.outputs.private_subnet_ids }}
public_subnet_ids: ${{ needs.deploy-vpc.outputs.public_subnet_ids }}
# Container & Application Configuration
ecr_repository_url: ${{ needs.build.outputs.ecr_repository_url }}
ecr_image_tag: ${{ needs.build.outputs.image_ref }}
# ECS & Compute Configuration
cpu: ${{ vars.API_CPU_STAGING || '512' }}
memory: ${{ vars.API_MEMORY_STAGING || '1024' }}
fargate_spot_weight: ${{ vars.API_FARGATE_SPOT_WEIGHT_STAGING || '90' }}
fargate_base: ${{ vars.API_FARGATE_BASE_STAGING || '0' }}
# Auto-scaling Configuration
min_capacity: ${{ vars.API_MIN_CAPACITY_STAGING || '1' }}
max_capacity: ${{ vars.API_MAX_CAPACITY_STAGING || '2' }}
cpu_target_value: ${{ vars.API_CPU_TARGET_STAGING || '70' }}
memory_target_value: ${{ vars.API_MEMORY_TARGET_STAGING || '80' }}
# Access Mode & Domain Configuration
# API_ACCESS_MODE_STAGING: 'public' (HTTPS with domain) or 'internal' (SSM tunnel)
api_access_mode: ${{ vars.API_ACCESS_MODE_STAGING || 'internal' }}
domain_name: ${{ vars.API_DOMAIN_NAME_STAGING || '' }}
domain_name_root: ${{ needs.stack-config.outputs.domain_name_root }}
# Database Configuration (from Postgres stack outputs)
# Uses RDS Proxy endpoint when RDS_PROXY_ENABLED_STAGING=true, direct endpoint otherwise.
# The proxy output is an empty string when the flag is off, which GHA's || coalesce treats as falsy.
database_endpoint: ${{ needs.deploy-postgres.outputs.database_proxy_endpoint || needs.deploy-postgres.outputs.database_endpoint }}
database_port: ${{ needs.deploy-postgres.outputs.database_port }}
# Lambda Configuration (container-based)
lambda_image_uri: ${{ needs.build.outputs.lambda_image }}
# Cache Configuration
valkey_url: ${{ needs.deploy-valkey.outputs.valkey_url }}
valkey_sg_id: ${{ needs.deploy-valkey.outputs.valkey_sg_id }}
bastion_sg_id: ${{ needs.deploy-bastion.outputs.bastion_sg_id }}
# OpenSearch Configuration (optional, empty when OPENSEARCH_ENABLED_STAGING=false)
opensearch_endpoint: ${{ needs.deploy-opensearch.outputs.opensearch_endpoint || '' }}
opensearch_sg_id: ${{ needs.deploy-opensearch.outputs.opensearch_sg_id || '' }}
# WAF Configuration (environment-specific, skipped for internal-only deployments)
waf_enabled: ${{ vars.WAF_ENABLED_STAGING || 'false' }}
waf_stack_name: ${{ needs.stack-config.outputs.waf_stack }}
waf_rate_limit_per_ip: ${{ vars.WAF_RATE_LIMIT_PER_IP || '3000' }}
waf_enable_geo_blocking: ${{ vars.WAF_GEO_BLOCKING_ENABLED || 'false' }}
waf_enable_aws_managed_rules: ${{ vars.WAF_AWS_MANAGED_RULES_ENABLED || 'true' }}
audit_enabled: ${{ vars.AUDIT_ENABLED_STAGING || 'false' }}
audit_stack_name: ${{ needs.stack-config.outputs.audit_stack }}
audit_retention_days: ${{ vars.AUDIT_RETENTION_DAYS || '400' }}
# S3 Bucket Configuration (from S3 stack outputs)
shared_processed_bucket_arn: ${{ needs.deploy-s3.outputs.shared_processed_bucket_arn }}
deployment_bucket_arn: ${{ needs.deploy-s3.outputs.deployment_bucket_arn }}
user_data_bucket_arn: ${{ needs.deploy-s3.outputs.user_data_bucket_arn }}
public_data_bucket_arn: ${{ needs.deploy-s3.outputs.public_data_bucket_arn }}
# Other Configuration
prometheus_stack_name: ${{ needs.deploy-prometheus.outputs.prometheus_stack_name || '' }}
aws_sns_alert_email: ${{ vars.AWS_SNS_ALERT_EMAIL }}
robosystems_app_url: ${{ vars.ROBOSYSTEMS_APP_URL_STAGING || 'https://staging.robosystems.ai' }}
roboledger_app_url: ${{ vars.ROBOLEDGER_APP_URL_STAGING || 'https://staging.roboledger.ai' }}
roboinvestor_app_url: ${{ vars.ROBOINVESTOR_APP_URL_STAGING || 'https://staging.roboinvestor.ai' }}
secrets: inherit
deploy-dagster:
needs:
[
runner,
create-deployment,
stack-config,
build,
deploy-vpc,
deploy-valkey,
deploy-postgres,
deploy-s3,
deploy-grafana,
deploy-prometheus,
deploy-opensearch,
deploy-bastion,
]
if: ${{ !cancelled() && !contains(needs.*.result, 'cancelled') && !contains(needs.*.result, 'failure') }}
uses: ./.github/workflows/deploy-dagster.yml
with:
# Stack & Repository Configuration
stack_name: ${{ needs.stack-config.outputs.dagster_stack }}
environment: ${{ vars.ENVIRONMENT_STAGING || 'staging' }}
# GHA Runner Configuration
runner_config: ${{ needs.runner.outputs.runner_config }}
# AWS Configuration
aws_region: ${{ vars.AWS_REGION || 'us-east-1' }}
vpc_id: ${{ needs.deploy-vpc.outputs.vpc_id }}
subnet_ids: ${{ needs.deploy-vpc.outputs.private_subnet_ids }}
# Container & Application Configuration
ecr_repository_url: ${{ needs.build.outputs.ecr_repository_url }}
ecr_image_tag: ${{ needs.build.outputs.image_ref }}
# Fargate Configuration
daemon_cpu: ${{ vars.DAGSTER_DAEMON_CPU_STAGING || '1024' }}
daemon_memory: ${{ vars.DAGSTER_DAEMON_MEMORY_STAGING || '2048' }}
webserver_cpu: ${{ vars.DAGSTER_WEBSERVER_CPU_STAGING || '512' }}
webserver_memory: ${{ vars.DAGSTER_WEBSERVER_MEMORY_STAGING || '1024' }}
webserver_desired_count: ${{ vars.DAGSTER_WEBSERVER_DESIRED_COUNT_STAGING || '0' }}
# Run Job Profiles: hardcoded in deploy-dagster.yml (Light/Standard/Compute/Heavy)
max_concurrent_runs: ${{ vars.DAGSTER_MAX_CONCURRENT_RUNS_STAGING || '20' }}
# Worker Configuration
worker_stack_name: ${{ needs.stack-config.outputs.worker_stack }}
graph_volumes_stack_name: ${{ needs.stack-config.outputs.graph_volumes_stack }}
worker_enabled: ${{ vars.WORKER_ENABLED_STAGING || 'false' }}
worker_cpu: ${{ vars.WORKER_CPU_STAGING || '512' }}
worker_memory: ${{ vars.WORKER_MEMORY_STAGING || '1024' }}
worker_desired_count: ${{ vars.WORKER_DESIRED_COUNT_STAGING || '1' }}
worker_min_count: ${{ vars.WORKER_MIN_COUNT_STAGING || '1' }}
worker_max_count: ${{ vars.WORKER_MAX_COUNT_STAGING || '5' }}
worker_autoscaling_enabled: ${{ vars.WORKER_AUTOSCALING_ENABLED_STAGING || 'false' }}
aws_sns_alert_email: ${{ vars.AWS_SNS_ALERT_EMAIL }}
# Capacity Provider Configuration (Spot vs On-Demand)
daemon_fargate_spot_weight: ${{ vars.DAGSTER_DAEMON_FARGATE_SPOT_WEIGHT_STAGING || '80' }}
webserver_fargate_spot_weight: ${{ vars.DAGSTER_WEBSERVER_FARGATE_SPOT_WEIGHT_STAGING || '80' }}
daemon_fargate_base: ${{ vars.DAGSTER_DAEMON_FARGATE_BASE_STAGING || '0' }}
webserver_fargate_base: ${{ vars.DAGSTER_WEBSERVER_FARGATE_BASE_STAGING || '0' }}
# Database Configuration (from Postgres stack outputs)
# Uses RDS Proxy endpoint when RDS_PROXY_ENABLED_STAGING=true, direct endpoint otherwise.
# Dagster daemon connections will pin on the proxy (LISTEN/NOTIFY, advisory locks) so
# they get no proxy benefit but pinning is neutral. The embedded worker stack benefits.
database_endpoint: ${{ needs.deploy-postgres.outputs.database_proxy_endpoint || needs.deploy-postgres.outputs.database_endpoint }}
database_port: ${{ needs.deploy-postgres.outputs.database_port }}
# Cache Configuration
valkey_url: ${{ needs.deploy-valkey.outputs.valkey_url }}
valkey_sg_id: ${{ needs.deploy-valkey.outputs.valkey_sg_id }}
# Database Security Group
database_sg_id: ${{ needs.deploy-postgres.outputs.database_sg_id }}
# OpenSearch Configuration (optional, empty when OPENSEARCH_ENABLED_STAGING=false)
opensearch_endpoint: ${{ needs.deploy-opensearch.outputs.opensearch_endpoint || '' }}
opensearch_sg_id: ${{ needs.deploy-opensearch.outputs.opensearch_sg_id || '' }}
# Bastion Access (for internal UI access)
bastion_sg_id: ${{ needs.deploy-bastion.outputs.bastion_sg_id }}
# Monitoring Configuration
container_insights_enabled: ${{ vars.DAGSTER_CONTAINER_INSIGHTS_STAGING || 'disabled' }}
# S3 Bucket Configuration (from S3 stack outputs)
shared_raw_bucket_arn: ${{ needs.deploy-s3.outputs.shared_raw_bucket_arn }}
shared_processed_bucket_arn: ${{ needs.deploy-s3.outputs.shared_processed_bucket_arn }}
deployment_bucket_arn: ${{ needs.deploy-s3.outputs.deployment_bucket_arn }}
user_data_bucket_arn: ${{ needs.deploy-s3.outputs.user_data_bucket_arn }}
public_data_bucket_arn: ${{ needs.deploy-s3.outputs.public_data_bucket_arn }}
logs_bucket_arn: ${{ needs.deploy-s3.outputs.logs_bucket_arn }}
public_data_cdn_url: ${{ needs.deploy-s3.outputs.public_data_cdn_url }}
robosystems_app_url: ${{ vars.ROBOSYSTEMS_APP_URL_STAGING || 'https://staging.robosystems.ai' }}
roboledger_app_url: ${{ vars.ROBOLEDGER_APP_URL_STAGING || 'https://staging.roboledger.ai' }}
roboinvestor_app_url: ${{ vars.ROBOINVESTOR_APP_URL_STAGING || 'https://staging.roboinvestor.ai' }}
secrets: inherit
# Service refresh - currently only graph containers (ECS services auto-deploy via version tags)
service-refresh:
needs: [runner, create-deployment, stack-config, deploy-graph]
if: ${{ !cancelled() && needs.deploy-graph.result == 'success' }}
uses: ./.github/workflows/service-refresh.yml
with:
environment: ${{ vars.ENVIRONMENT_STAGING || 'staging' }}
runner_config: ${{ needs.runner.outputs.runner_config }}
aws_region: ${{ vars.AWS_REGION || 'us-east-1' }}
# Graph refresh (EC2 containers via SSM)
graph_refresh_enabled: ${{ inputs.graph_container_refresh != false }}
graph_node_types: "writer"
# Pre-refresh busy-counter wait (protects in-flight materialization
# on the shared-tier SEC master from mid-op cycling)
max_wait_minutes: ${{ inputs.graph_refresh_max_wait_minutes || '30' }}
force_ignore_busy: ${{ (inputs.graph_refresh_force_ignore_busy && 'true') || 'false' }}
# API/Dagster/Worker refresh disabled - ECS auto-deploys via version tags in task definitions
api_refresh_enabled: "false"
api_stack_name: ${{ needs.stack-config.outputs.api_stack }}
dagster_refresh_enabled: "false"
worker_refresh_enabled: "false"
secrets: inherit
deployment-successful:
needs:
[
runner,
build,
stack-config,
create-deployment,
deploy-vpc,
deploy-graph,
deploy-valkey,
deploy-postgres,
deploy-s3,
deploy-grafana,
deploy-prometheus,
deploy-opensearch,
deploy-bastion,
deploy-api,
deploy-dagster,
service-refresh,
]
if: |
always() &&
needs.runner.result == 'success' &&
needs.build.result == 'success' &&
needs.stack-config.result == 'success' &&
needs.create-deployment.result == 'success' &&
needs.deploy-vpc.result == 'success' &&
needs.deploy-graph.result == 'success' &&
needs.deploy-valkey.result == 'success' &&
needs.deploy-postgres.result == 'success' &&
needs.deploy-s3.result == 'success' &&
(needs.deploy-grafana.result == 'success' || needs.deploy-grafana.result == 'skipped') &&
(needs.deploy-prometheus.result == 'success' || needs.deploy-prometheus.result == 'skipped') &&
(needs.deploy-opensearch.result == 'success' || needs.deploy-opensearch.result == 'skipped') &&
needs.deploy-bastion.result == 'success' &&
needs.deploy-api.result == 'success' &&
needs.deploy-dagster.result == 'success' &&
(needs.service-refresh.result == 'success' || needs.service-refresh.result == 'skipped')
runs-on: ${{ fromJSON(needs.runner.outputs.runner_config) }}
outputs:
deployment_id: ${{ needs.create-deployment.outputs.deployment_id }}
steps:
- name: Checkout Code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
repository: ${{ github.repository }}
ref: ${{ github.ref }}
token: ${{ github.token }}
- name: Generate Deployment Summary
env:
API_ACCESS_MODE: ${{ vars.API_ACCESS_MODE_STAGING || 'internal' }}
API_DOMAIN: ${{ vars.API_DOMAIN_NAME_STAGING || '' }}
LBUG_LARGE_ENABLED: ${{ vars.LBUG_LARGE_ENABLED_STAGING || 'false' }}
LBUG_XLARGE_ENABLED: ${{ vars.LBUG_XLARGE_ENABLED_STAGING || 'false' }}
LBUG_SHARED_ENABLED: ${{ vars.LBUG_SHARED_ENABLED_STAGING || 'false' }}
run: |
cat >> $GITHUB_STEP_SUMMARY << EOF
## Staging Deployment Successful
| Component | Details |
|-----------|---------|
| Environment | staging |
| Image Tag | \`${{ needs.build.outputs.image_tag }}\` |
| Image | \`${{ needs.build.outputs.image }}\` |
| Commit | \`${{ github.sha }}\` |
| Ref | \`${{ github.ref_name }}\` |
| Triggered by | @${{ github.actor }} |
### API Access
EOF
if [ "$API_ACCESS_MODE" = "public" ] && [ -n "$API_DOMAIN" ]; then
cat >> $GITHUB_STEP_SUMMARY << EOF
- **Mode**: Public (HTTPS)
- **URL**: https://${API_DOMAIN}
- **Admin CLI**: \`just admin staging <command>\` (requires \`just tunnel staging api-internal\`; the CLI targets the tunnel on :18000)
EOF
else
cat >> $GITHUB_STEP_SUMMARY << EOF
- **Mode**: Internal (SSM tunnel required)
- **Connect**: \`just tunnel staging all\`
- **API**: http://localhost:18000
- **Admin CLI**: \`just admin staging <command>\`
EOF
fi
cat >> $GITHUB_STEP_SUMMARY << EOF
### LadybugDB Graph Clusters
| Tier | Status |
|------|--------|
| Standard | Enabled |
EOF
if [ "$LBUG_LARGE_ENABLED" = "true" ]; then
echo "| Large | Enabled |" >> $GITHUB_STEP_SUMMARY
fi
if [ "$LBUG_XLARGE_ENABLED" = "true" ]; then
echo "| XLarge | Enabled |" >> $GITHUB_STEP_SUMMARY
fi
if [ "$LBUG_SHARED_ENABLED" = "true" ]; then
echo "| Shared | Enabled |" >> $GITHUB_STEP_SUMMARY
fi
cat >> $GITHUB_STEP_SUMMARY << EOF
### All Services
Connect to all services with one command:
\`\`\`
just tunnel staging all
\`\`\`
| Service | Local Port |
|---------|------------|
| API | http://localhost:18000 |
| Dagster UI | http://localhost:18002 |
| PostgreSQL | localhost:15432 |
| Valkey | localhost:16379 |
### Next Steps
- [Bootstrap Guide](https://robosystems.ai/docs/technical/bootstrap-guide) - Initial setup
- [Architecture Overview](https://robosystems.ai/docs/technical/architecture-overview) - System architecture
EOF
- name: Update Deployment Status
uses: chrnorm/deployment-status@6df8d036fd2fee9eb82936733953da1f8382b41e # v2
with:
token: ${{ github.token }}
state: success
deployment-id: ${{ needs.create-deployment.outputs.deployment_id }}
description: "Staging Deployment Succeeded"
handle-deployment-failure:
needs:
[
runner,
create-deployment,
deploy-vpc,
deploy-graph,
deploy-valkey,
deploy-postgres,
deploy-s3,
deploy-grafana,
deploy-prometheus,
deploy-bastion,
deploy-api,
deploy-dagster,
service-refresh,
]
if: always() && contains(needs.*.result, 'failure')
runs-on: ${{ fromJSON(needs.runner.outputs.runner_config) }}
steps:
- name: Checkout Code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
repository: ${{ github.repository }}
ref: ${{ github.ref }}
token: ${{ github.token }}
- name: Handle Deployment Failure
uses: ./.github/actions/handle-deployment-failure
with:
deployment-id: ${{ needs.create-deployment.outputs.deployment_id }}
environment: staging
github-token: ${{ github.token }}