From 1ede5f2668a754897fc2bec872b3a355990308ff Mon Sep 17 00:00:00 2001 From: Andy Potanin Date: Sat, 2 May 2026 12:06:32 -0400 Subject: [PATCH 1/8] Add generic state backend override inputs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - state_backend: override backend type (s3, http, consul, etc.) - state_backend_config: key=value backend config lines - state_prefix_key: backend-specific path key (prefix for gcs, key for s3) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- action.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/action.yml b/action.yml index 8301afc..deded2a 100644 --- a/action.yml +++ b/action.yml @@ -82,6 +82,18 @@ inputs: description: "Slack incoming webhook URL for deployment notifications" required: false + # State backend + state_backend: + description: "Override state backend type (e.g. s3, http, consul). Unset = default gcs." + required: false + state_backend_config: + description: "Backend config as key=value lines (e.g. bucket=my-bucket newline region=us-east-1). Required when state_backend is set." + required: false + state_prefix_key: + description: "Backend-specific key for state path prefix (gcs=prefix, s3=key). Default: prefix" + required: false + default: "prefix" + # Config source_dir: description: "Source directory for .rabbit configs" @@ -494,6 +506,9 @@ runs: DOCKERHUB_USERNAME: ${{ inputs.dockerhub_username }} DOCKERHUB_TOKEN: ${{ inputs.dockerhub_helm_token || inputs.dockerhub_token }} SHARED_PROJECT: ${{ inputs.shared_project }} + STATE_BACKEND: ${{ inputs.state_backend }} + STATE_BACKEND_CONFIG: ${{ inputs.state_backend_config }} + STATE_PREFIX_KEY: ${{ inputs.state_prefix_key }} REPO_OWNER: ${{ github.repository_owner }} REPO_NAME: ${{ github.event.repository.name }} run: | @@ -537,6 +552,9 @@ runs: -e "DOCKERHUB_USERNAME=${DOCKERHUB_USERNAME:-}" \ -e "DOCKERHUB_TOKEN=${DOCKERHUB_TOKEN:-}" \ -e "SHARED_PROJECT=${SHARED_PROJECT:-}" \ + -e "STATE_BACKEND=${STATE_BACKEND:-}" \ + -e "STATE_BACKEND_CONFIG=${STATE_BACKEND_CONFIG:-}" \ + -e "STATE_PREFIX_KEY=${STATE_PREFIX_KEY:-prefix}" \ -e "GOOGLE_APPLICATION_CREDENTIALS=/github/workspace/gcp-credentials.json" \ "${aws_env[@]}" \ "$IMAGE" \ From 1dafc18097f1188e0e832dc163167a6b605ebf1b Mon Sep 17 00:00:00 2001 From: Andy Potanin Date: Sat, 2 May 2026 12:27:26 -0400 Subject: [PATCH 2/8] make GCP auth optional for AWS-only deployments --- action.yml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/action.yml b/action.yml index deded2a..5aab850 100644 --- a/action.yml +++ b/action.yml @@ -13,10 +13,10 @@ inputs: required: true gcp_auth_provider: description: "GCP Workload Identity Federation provider resource name" - required: true + required: false gcp_service_account: description: "GCP service account email for Workload Identity" - required: true + required: false # Auth aws_role_arn: @@ -417,7 +417,7 @@ runs: # ========================================================================= - name: Authenticate with Google Cloud id: gcp-auth - if: steps.safety.outputs.should_deploy == 'true' + if: steps.safety.outputs.should_deploy == 'true' && inputs.gcp_auth_provider != '' && inputs.gcp_service_account != '' uses: google-github-actions/auth@v3 with: workload_identity_provider: ${{ inputs.gcp_auth_provider }} @@ -448,7 +448,7 @@ runs: # ========================================================================= - name: Prepare Workload Identity credentials id: prepare-creds - if: steps.safety.outputs.should_deploy == 'true' + if: steps.safety.outputs.should_deploy == 'true' && steps.gcp-auth.outputs.credentials_file_path != '' shell: bash env: CRED_FILE_PATH: ${{ steps.gcp-auth.outputs.credentials_file_path }} @@ -530,6 +530,12 @@ runs: aws_env+=(-e "AWS_REGION=") fi + # Build GCP env vars (may be empty if GCP not configured) + gcp_env=() + if [[ -f "$(pwd)/gcp-credentials.json" ]]; then + gcp_env+=(-e "GOOGLE_APPLICATION_CREDENTIALS=/github/workspace/gcp-credentials.json") + fi + echo "🐰 Running Rabbit Automation Action..." docker run --rm \ -v "$(pwd):/github/workspace" \ @@ -555,7 +561,7 @@ runs: -e "STATE_BACKEND=${STATE_BACKEND:-}" \ -e "STATE_BACKEND_CONFIG=${STATE_BACKEND_CONFIG:-}" \ -e "STATE_PREFIX_KEY=${STATE_PREFIX_KEY:-prefix}" \ - -e "GOOGLE_APPLICATION_CREDENTIALS=/github/workspace/gcp-credentials.json" \ + ${gcp_env[@]+"${gcp_env[@]}"} \ "${aws_env[@]}" \ "$IMAGE" \ /usr/local/bin/entrypoint.sh From e8c4ddda4cf4cbebc0deb3a6293110553273c59f Mon Sep 17 00:00:00 2001 From: Andy Potanin Date: Sat, 2 May 2026 12:51:06 -0400 Subject: [PATCH 3/8] externalize auth: remove GCP/AWS auth steps from action, caller handles auth --- action.yml | 115 ++++++++++++++++------------------------------------- 1 file changed, 35 insertions(+), 80 deletions(-) diff --git a/action.yml b/action.yml index 5aab850..ba51bd4 100644 --- a/action.yml +++ b/action.yml @@ -9,22 +9,8 @@ branding: inputs: # Required project_id: - description: "GCP project ID for Terraform state and resource operations" + description: "Project identifier for state isolation and resource scoping" required: true - gcp_auth_provider: - description: "GCP Workload Identity Federation provider resource name" - required: false - gcp_service_account: - description: "GCP service account email for Workload Identity" - required: false - - # Auth - aws_role_arn: - description: "AWS IAM OIDC role ARN for Route53, CloudFront, WAF, ACM" - required: false - aws_region: - description: "AWS region" - required: false # Docker dockerhub_username: @@ -47,7 +33,7 @@ inputs: required: false default: "apply" plan_only: - description: "Override plan mode (auto-detected: PRs and schedules → true, pushes → false)" + description: "Override plan mode (auto-detected: PRs and schedules -> true, pushes -> false)" required: false environment: description: "Override environment name (auto-detected from branch name)" @@ -84,13 +70,13 @@ inputs: # State backend state_backend: - description: "Override state backend type (e.g. s3, http, consul). Unset = default gcs." + description: "Override state backend type (e.g. s3, http, consul, azurerm). Unset = default gcs." required: false state_backend_config: description: "Backend config as key=value lines (e.g. bucket=my-bucket newline region=us-east-1). Required when state_backend is set." required: false state_prefix_key: - description: "Backend-specific key for state path prefix (gcs=prefix, s3=key). Default: prefix" + description: "Backend-specific key for state path prefix (gcs=prefix, s3=key, azurerm=key). Default: prefix" required: false default: "prefix" @@ -373,7 +359,6 @@ runs: TRIGGER: ${{ github.event_name }} R2A_VERSION: ${{ inputs.r2a_version }} MULTI_REPO: ${{ inputs.multi_repo }} - HAS_AWS: ${{ inputs.aws_role_arn != '' && inputs.aws_region != '' }} run: | config_items="$(printf '%s' "${DETECTED_CONFIGS:-none}" | sed "s/#{Environment}/$ENV_NAME/g" | tr ',' '\n' | sed 's/^ *//; s/ *$//' | sed '/^$/d')" @@ -391,7 +376,6 @@ runs: echo "| Lifecycle | \`${LIFECYCLE:-unknown}\` |" echo "| Plan Only | \`${PLAN_ONLY:-true}\` |" echo "| Multi Repo | \`${MULTI_REPO:-false}\` |" - echo "| AWS Enabled | \`${HAS_AWS}\` |" echo "" echo "### Detected Configs" echo "" @@ -413,28 +397,7 @@ runs: } >> "$GITHUB_STEP_SUMMARY" # ========================================================================= - # 10. Authenticate with Google Cloud - # ========================================================================= - - name: Authenticate with Google Cloud - id: gcp-auth - if: steps.safety.outputs.should_deploy == 'true' && inputs.gcp_auth_provider != '' && inputs.gcp_service_account != '' - uses: google-github-actions/auth@v3 - with: - workload_identity_provider: ${{ inputs.gcp_auth_provider }} - service_account: ${{ inputs.gcp_service_account }} - - # ========================================================================= - # 11. Configure AWS credentials (optional) - # ========================================================================= - - name: Configure AWS credentials - if: steps.safety.outputs.should_deploy == 'true' && inputs.aws_role_arn != '' && inputs.aws_region != '' - uses: aws-actions/configure-aws-credentials@v6 - with: - role-to-assume: ${{ inputs.aws_role_arn }} - aws-region: ${{ inputs.aws_region }} - - # ========================================================================= - # 12. Prepare artifacts directory + # 10. Prepare artifacts directory # ========================================================================= - name: Prepare artifacts directory if: steps.safety.outputs.should_deploy == 'true' @@ -444,22 +407,7 @@ runs: chmod -R 777 terraform/plans # ========================================================================= - # 13. Prepare GCP credentials for Docker volume mount - # ========================================================================= - - name: Prepare Workload Identity credentials - id: prepare-creds - if: steps.safety.outputs.should_deploy == 'true' && steps.gcp-auth.outputs.credentials_file_path != '' - shell: bash - env: - CRED_FILE_PATH: ${{ steps.gcp-auth.outputs.credentials_file_path }} - run: | - set -euo pipefail - cp "$CRED_FILE_PATH" gcp-credentials.json - chmod 644 gcp-credentials.json - echo "credentials_path=$(pwd)/gcp-credentials.json" >> "$GITHUB_OUTPUT" - - # ========================================================================= - # 14. Docker login and pull R2A image + # 11. Docker login and pull R2A image # ========================================================================= - name: Docker login and pull R2A image if: steps.safety.outputs.should_deploy == 'true' @@ -473,16 +421,13 @@ runs: IMAGE="usabilitydynamics/rabbit-automation-action:${R2A_VERSION}" if [[ -n "$DOCKERHUB_USERNAME" && -n "$DOCKERHUB_TOKEN" ]]; then - echo "🔐 Logging in to Docker Hub..." echo "$DOCKERHUB_TOKEN" | docker login -u "$DOCKERHUB_USERNAME" --password-stdin fi - echo "📦 Pulling $IMAGE..." docker pull "$IMAGE" - echo "✓ Image pulled successfully" # ========================================================================= - # 15. Run Rabbit Automation Action (Terraform engine) + # 12. Run Rabbit Automation Action (IaC engine) # ========================================================================= - name: Run Rabbit Automation Action id: terraform @@ -516,27 +461,36 @@ runs: IMAGE="usabilitydynamics/rabbit-automation-action:${R2A_VERSION}" ARTIFACTS_PATH="terraform/plans" - # Build AWS env vars (may be empty if AWS not configured) + # Build cloud credential env vars from caller's auth steps + # AWS: auto-detected from aws-actions/configure-aws-credentials aws_env=() if [[ -n "${AWS_ACCESS_KEY_ID:-}" ]]; then aws_env+=(-e "AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}") aws_env+=(-e "AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}") - aws_env+=(-e "AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN}") + aws_env+=(-e "AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN:-}") aws_env+=(-e "AWS_REGION=${AWS_REGION:-}") - else - aws_env+=(-e "AWS_ACCESS_KEY_ID=") - aws_env+=(-e "AWS_SECRET_ACCESS_KEY=") - aws_env+=(-e "AWS_SESSION_TOKEN=") - aws_env+=(-e "AWS_REGION=") fi - # Build GCP env vars (may be empty if GCP not configured) + # GCP: auto-detected from google-github-actions/auth gcp_env=() - if [[ -f "$(pwd)/gcp-credentials.json" ]]; then + if [[ -n "${GOOGLE_APPLICATION_CREDENTIALS:-}" && -f "${GOOGLE_APPLICATION_CREDENTIALS}" ]]; then + cp "${GOOGLE_APPLICATION_CREDENTIALS}" "$(pwd)/gcp-credentials.json" + chmod 644 "$(pwd)/gcp-credentials.json" gcp_env+=(-e "GOOGLE_APPLICATION_CREDENTIALS=/github/workspace/gcp-credentials.json") fi - echo "🐰 Running Rabbit Automation Action..." + # Azure: auto-detected from azure/login + azure_env=() + if [[ -n "${ARM_CLIENT_ID:-}" ]]; then + azure_env+=(-e "ARM_CLIENT_ID=${ARM_CLIENT_ID}") + azure_env+=(-e "ARM_CLIENT_SECRET=${ARM_CLIENT_SECRET:-}") + azure_env+=(-e "ARM_TENANT_ID=${ARM_TENANT_ID:-}") + azure_env+=(-e "ARM_SUBSCRIPTION_ID=${ARM_SUBSCRIPTION_ID:-}") + fi + if [[ -n "${ARM_ACCESS_KEY:-}" ]]; then + azure_env+=(-e "ARM_ACCESS_KEY=${ARM_ACCESS_KEY}") + fi + docker run --rm \ -v "$(pwd):/github/workspace" \ -e "GCP_PROJECT=${PROJECT_ID}" \ @@ -562,12 +516,13 @@ runs: -e "STATE_BACKEND_CONFIG=${STATE_BACKEND_CONFIG:-}" \ -e "STATE_PREFIX_KEY=${STATE_PREFIX_KEY:-prefix}" \ ${gcp_env[@]+"${gcp_env[@]}"} \ - "${aws_env[@]}" \ + ${aws_env[@]+"${aws_env[@]}"} \ + ${azure_env[@]+"${azure_env[@]}"} \ "$IMAGE" \ /usr/local/bin/entrypoint.sh echo "" - echo "📋 Artifacts generated:" + echo "Artifacts generated:" ls -la "$ARTIFACTS_PATH" 2>/dev/null || echo "No artifacts directory found" # Extract CloudFront distribution ID from terraform outputs @@ -588,7 +543,7 @@ runs: echo "aws_cloudfront_distribution_id=${cloudfront_distribution_id}" >> "$GITHUB_OUTPUT" # ========================================================================= - # 16. Upload terraform artifacts + # 13. Upload terraform artifacts # ========================================================================= - name: Upload terraform artifacts if: steps.safety.outputs.should_deploy == 'true' @@ -600,7 +555,7 @@ runs: if-no-files-found: warn # ========================================================================= - # 17. Render plan summary + # 14. Render plan summary # ========================================================================= - name: Render plan summary id: plan-summary @@ -649,7 +604,7 @@ runs: bash "${GITHUB_ACTION_PATH}/bin/render-plan-summary.sh" "$summary_file" "$ARTIFACTS_PATH/plan-summary.md" # ========================================================================= - # 18. Write deployment summary + # 15. Write deployment summary # ========================================================================= - name: Write deployment summary if: always() && steps.safety.outputs.should_deploy == 'true' @@ -693,7 +648,7 @@ runs: fi # ========================================================================= - # 19. Post PR comment with plan summary + # 16. Post PR comment with plan summary # ========================================================================= - name: Post PR comment with plan summary if: github.event_name == 'pull_request' && steps.plan-summary.outputs.has_changes == 'true' @@ -754,7 +709,7 @@ runs: } # ========================================================================= - # 20. Upload terraform plan files + # 17. Upload terraform plan files # ========================================================================= - name: Upload terraform plans if: steps.plan-summary.outputs.has_changes == 'true' @@ -766,7 +721,7 @@ runs: if-no-files-found: ignore # ========================================================================= - # 21. Send Slack notification + # 18. Send Slack notification # ========================================================================= - name: Send Slack notification if: always() && inputs.slack_webhook != '' && (steps.plan-summary.outputs.has_changes == 'true' || steps.terraform.outcome == 'failure' || steps.safety.outcome == 'failure') From e801ae3f6b1aaf7f3db1393c82ba358c2685fdbf Mon Sep 17 00:00:00 2001 From: Andy Potanin Date: Sat, 2 May 2026 12:56:09 -0400 Subject: [PATCH 4/8] update README for externalized auth, OpenTofu, and multi-cloud backends --- README.md | 362 +++++++++++++++++++----------------------------------- 1 file changed, 125 insertions(+), 237 deletions(-) diff --git a/README.md b/README.md index 98ef1f3..13b0fdc 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **Declare cloud infrastructure in YAML. Deploy with `git push`.** -A GitHub Marketplace composite action that discovers YAML configuration from your `.rabbit/` directory and deploys cloud infrastructure across AWS, GCP, and Kubernetes using Terraform — all from a single `uses:` step. +A GitHub Marketplace composite action that discovers YAML configuration from your `.rabbit/` directory and deploys cloud infrastructure across AWS, GCP, Azure, and Kubernetes using OpenTofu — all from a single `uses:` step. --- @@ -22,21 +22,14 @@ on: branches: ["production", "staging", "develop-*"] paths: [".rabbit/**"] delete: - schedule: - - cron: "0 2 * * *" workflow_dispatch: inputs: plan_only: description: "Plan only (no apply)" type: boolean default: true - environment: - description: "Target environment" - type: choice - options: [development, staging, production] - default: development terraform_action: - description: "Terraform action" + description: "Action" type: choice options: [apply, destroy] default: apply @@ -52,17 +45,75 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: udx/github-rabbit-action@v1 + # Authenticate with your cloud provider(s) before calling the action + - uses: google-github-actions/auth@v3 + with: + workload_identity_provider: ${{ vars.GCP_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ vars.GCP_SERVICE_ACCOUNT }} + + - uses: aws-actions/configure-aws-credentials@v6 + with: + role-to-assume: ${{ secrets.AWS_GITHUB_ACTIONS_ROLE_ARN }} + aws-region: us-east-1 + + - uses: udx/github-rabbit-action@v5 with: project_id: ${{ vars.GCP_PROJECT_ID }} - gcp_auth_provider: ${{ vars.GCP_AUTH_PROVIDER }} - gcp_service_account: ${{ vars.GCP_SERVICE_ACCOUNT }} - aws_region: ${{ vars.AWS_REGION }} - aws_role_arn: ${{ secrets.AWS_GITHUB_ACTIONS_ROLE_ARN }} - slack_webhook: ${{ secrets.SLACK_WEBHOOK_ROUTINE }} dockerhub_username: ${{ vars.DOCKERHUB_USER_LOGIN }} dockerhub_token: ${{ secrets.DOCKERHUB_TOKEN_PULL_R2A }} - r2a_version: "4.8.0" + slack_webhook: ${{ secrets.SLACK_WEBHOOK_ROUTINE }} + terraform_action: ${{ inputs.terraform_action || 'apply' }} +``` + +#### AWS-only with S3 state backend + +```yaml +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: aws-actions/configure-aws-credentials@v6 + with: + role-to-assume: ${{ secrets.AWS_GITHUB_ACTIONS_ROLE_ARN }} + aws-region: us-east-1 + + - uses: udx/github-rabbit-action@v5 + with: + project_id: my-project + state_backend: s3 + state_backend_config: | + bucket = "my-tfstate-bucket" + region = "us-east-1" + state_prefix_key: key + terraform_action: ${{ inputs.terraform_action || 'apply' }} +``` + +#### Azure with azurerm state backend + +```yaml +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: azure/login@v2 + with: + client-id: ${{ vars.AZURE_CLIENT_ID }} + tenant-id: ${{ vars.AZURE_TENANT_ID }} + subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} + + - uses: udx/github-rabbit-action@v5 + with: + project_id: my-project + state_backend: azurerm + state_backend_config: | + storage_account_name = "mytfstate" + container_name = "tfstate" + resource_group_name = "my-rg" + state_prefix_key: key terraform_action: ${{ inputs.terraform_action || 'apply' }} ``` @@ -76,13 +127,14 @@ Create YAML files inside `.rabbit//`: services: - module: aws-route53 id: my-domain - domain: example.com - records: - - type: A - name: "" - alias: - name: d1234.cloudfront.net - zone_id: Z2FDTNDATAQYW2 + configurations: + domain: example.com + records: + - type: A + name: "" + alias: + name: d1234.cloudfront.net + zone_id: Z2FDTNDATAQYW2 ``` #### `.rabbit/production/20-cdn.yaml` @@ -91,30 +143,12 @@ services: services: - module: aws-cloudfront-distribution id: my-cdn-#{Environment} - domain: example.com - origins: - - domain_name: my-app.example.com - origin_id: app-origin -``` - -#### `.rabbit/production/30-app.yaml` - -```yaml -services: - - module: k8s-namespace - id: my-app - - - module: k8s-deployment - id: my-app - image: my-org/my-app:latest - replicas: 2 - ports: - - containerPort: 8080 - - - module: k8s-http-gateway-route - id: my-app - hostname: my-app.example.com - service_port: 8080 + configurations: + domain: example.com + origins: + app: + domain_name: my-app.example.com + origin_id: app-origin ``` ### 3. Push and watch @@ -125,6 +159,36 @@ services: --- +## Authentication + +Cloud authentication is **your workflow's responsibility**. The action auto-detects credentials from the environment: + +| Provider | Auth Action | Detected Via | +| --- | --- | --- | +| GCP | `google-github-actions/auth@v3` | `GOOGLE_APPLICATION_CREDENTIALS` file | +| AWS | `aws-actions/configure-aws-credentials@v6` | `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `AWS_SESSION_TOKEN` | +| Azure | `azure/login@v2` | `ARM_CLIENT_ID` / `ARM_CLIENT_SECRET` / `ARM_TENANT_ID` / `ARM_SUBSCRIPTION_ID` | + +Only include auth steps for the providers you need. The action passes detected credentials to the IaC engine container automatically. + +--- + +## State Backend + +By default, Terraform/OpenTofu state is stored in GCS (Google Cloud Storage). Override with any supported backend: + +| Backend | `state_backend` | `state_prefix_key` | Config Keys | +| --- | --- | --- | --- | +| GCS (default) | — | `prefix` | `bucket` | +| S3 | `s3` | `key` | `bucket`, `region` | +| Azure Blob | `azurerm` | `key` | `storage_account_name`, `container_name`, `resource_group_name` | +| HTTP | `http` | — | `address`, `lock_address`, `unlock_address` | +| Consul | `consul` | — | `address`, `path` | + +The backend override is injected at runtime using OpenTofu override files — no module changes needed. + +--- + ## How It Works ``` @@ -147,20 +211,16 @@ services: └─────────────┬──────────────┘ │ ┌─────────────▼──────────────┐ - │ 3. Cloud Auth │ - │ GCP Workload Identity │ - │ AWS OIDC (optional) │ - └─────────────┬──────────────┘ - │ - ┌─────────────▼──────────────┐ - │ 4. Terraform Engine │ + │ 3. IaC Engine │ │ Docker: r2a container │ + │ OpenTofu (default) or │ + │ Terraform (via IAC_TOOL) │ │ Per-service init/plan/ │ │ apply in deploy order │ └─────────────┬──────────────┘ │ ┌─────────────▼──────────────┐ - │ 5. Reporting │ + │ 4. Reporting │ │ Plan summary table │ │ PR comment │ │ GitHub step summary │ @@ -285,7 +345,8 @@ Each service in your YAML config follows this structure: services: - module: # Required: Terraform module to use id: # Required: Unique identifier for this service - # ... module-specific fields + configurations: + # ... module-specific fields ``` ### Placeholders @@ -302,16 +363,6 @@ Use `#{Variable}` syntax in YAML values — they're replaced at runtime: | `#{Namespace}` | Kubernetes namespace (derived from repo name) | | `#{SharedProject}` | Shared GCP project ID | -Example: - -```yaml -services: - - module: k8s-deployment - id: my-app-#{Environment} - namespace: #{Namespace} - image: gcr.io/#{GcpProject}/my-app:latest -``` - ### GCP Secret Manager References Reference secrets directly in your YAML: @@ -320,132 +371,12 @@ Reference secrets directly in your YAML: services: - module: k8s-secret id: app-secrets - data: - DATABASE_URL: gcp://projects/my-project/secrets/db-url/versions/latest + configurations: + data: + DATABASE_URL: gcp://projects/my-project/secrets/db-url/versions/latest ``` -The action automatically resolves `gcp://` prefixed values to actual secret values at deploy time. - ---- - -## Setup Guide - -### Required GitHub Variables - -Set these in your repository or organization settings → Variables: - -| Variable | Description | -| --- | --- | -| `GCP_PROJECT_ID` | Google Cloud project ID | -| `GCP_AUTH_PROVIDER` | GCP Workload Identity Provider resource name | -| `GCP_SERVICE_ACCOUNT` | GCP Service Account email | - -### Optional GitHub Variables - -| Variable | Description | -| --- | --- | -| `AWS_REGION` | AWS region (e.g., `us-east-1`) | -| `DOCKERHUB_USER_LOGIN` | Docker Hub username for image pulls | -| `K8S_CLUSTER_NAME` | GKE cluster name | -| `NEWRELIC_ACCOUNT_ID` | New Relic account ID | -| `SHARED_PROJECT` | Shared GCP project for cross-project access | - -### Required GitHub Secrets - -| Secret | Description | -| --- | --- | -| `SLACK_WEBHOOK_ROUTINE` | Slack incoming webhook for notifications | - -### Optional GitHub Secrets - -| Secret | Description | -| --- | --- | -| `AWS_GITHUB_ACTIONS_ROLE_ARN` | AWS IAM OIDC role for Route53/CloudFront/WAF/ACM | -| `DOCKERHUB_TOKEN_PULL_R2A` | Docker Hub token (paired with `DOCKERHUB_USER_LOGIN`) | -| `DOCKERHUB_HELM_TOKEN` | Docker Hub token for Helm OCI charts | -| `NEWRELIC_API_KEY` | New Relic API key | - -### Workflow Permissions - -```yaml -permissions: - contents: read - pull-requests: write # For PR comments with plan summary - id-token: write # For GCP Workload Identity & AWS OIDC -``` - ---- - -## Advanced Usage - -### Multi-Environment Overrides - -Override specific values per environment using subdirectories: - -``` -.rabbit/ -└── production/ - ├── 10-infra.yaml # Base production config - └── us-east-1/ - └── 10-infra.yaml # Overrides for us-east-1 environment -``` - -Files in subdirectories are deep-merged on top of the parent directory files. Services with matching `module::id` pairs are merged, not duplicated. - -### Multi-Repo Projects - -For monorepo or multi-repo setups where multiple repositories share infrastructure: - -```yaml -- uses: udx/github-rabbit-action@v1 - with: - multi_repo: "true" - shared_project: "shared-infra-project" - # ... other inputs -``` - -This isolates Terraform state per repository while allowing shared GCP project access. - -### Pinning R2A Version - -Always pin to a specific version for reproducible builds: - -```yaml -- uses: udx/github-rabbit-action@v1 - with: - r2a_version: "4.8.0" -``` - -### Ephemeral Environments (Branch Delete → Destroy) - -Add `delete` to your workflow triggers and matching feature branches: - -```yaml -on: - delete: # Triggers destroy when branch is deleted - push: - branches: ["production", "staging", "develop-*"] -``` - -When a `develop-*` branch is deleted, the action automatically runs `terraform destroy` for that environment. - -### Debug Mode - -Enable detailed config output in logs: - -```yaml -- uses: udx/github-rabbit-action@v1 - with: - print_config: "true" -``` - -### Manual Dispatch with Safety Controls - -The workflow dispatch inputs provide safe manual control: - -- **Plan only = true** → preview changes without applying -- **Environment = production** + **Plan only = false** → blocked (safety guardrail) -- **Terraform action = destroy** + **Environment = production** → blocked +The action automatically resolves `gcp://` prefixed values to actual secret values at deploy time (requires GCP auth). --- @@ -453,11 +384,7 @@ The workflow dispatch inputs provide safe manual control: | Input | Required | Default | Description | | --- | --- | --- | --- | -| `project_id` | ✅ | — | GCP project ID | -| `gcp_auth_provider` | ✅ | — | GCP Workload Identity Provider | -| `gcp_service_account` | ✅ | — | GCP Service Account email | -| `aws_role_arn` | — | — | AWS IAM OIDC role ARN | -| `aws_region` | — | — | AWS region | +| `project_id` | yes | — | Project identifier for state isolation | | `dockerhub_username` | — | — | Docker Hub username | | `dockerhub_token` | — | — | Docker Hub pull token | | `dockerhub_helm_token` | — | — | Docker Hub Helm OCI token | @@ -472,6 +399,9 @@ The workflow dispatch inputs provide safe manual control: | `newrelic_account_id` | — | — | New Relic account ID | | `newrelic_api_key` | — | — | New Relic API key | | `slack_webhook` | — | — | Slack webhook URL | +| `state_backend` | — | `gcs` | Backend type (`s3`, `azurerm`, `http`, `consul`) | +| `state_backend_config` | — | — | Backend config as key=value lines | +| `state_prefix_key` | — | `prefix` | Backend key for state path | | `source_dir` | — | `.rabbit` | Config source directory | | `github_token` | — | `github.token` | GitHub token for PR comments | @@ -491,48 +421,6 @@ The workflow dispatch inputs provide safe manual control: --- -## What You'll See - -### GitHub Step Summary - -Every run writes a configuration summary and deployment results to the GitHub Actions step summary — visible directly on the Actions run page. - -### PR Comments - -Pull requests get an automatically updated comment with a detailed Terraform plan breakdown: - -``` -## Terraform Plan Summary - -| Module / Service | Add | Change | Destroy | -| --- | --- | --- | --- | -| **Total** | 5 | 2 | 0 | -| `aws-cloudfront-distribution/my-cdn` | 0 | 1 | 0 | -| `k8s-deployment/my-app` | 3 | 1 | 0 | -| `k8s-http-gateway-route/my-app` | 2 | 0 | 0 | -``` - -### Slack Notifications - -Notifications are sent when: -- Infrastructure changes are detected or applied -- Any step fails - -Notifications include environment, change counts, failure stage, and a link to the action run. - ---- - -## Pro Tips - -- **Use a Docker Hub token** (`dockerhub_username` + `dockerhub_token`) to prevent rate limits when pulling the R2A image -- **Pin `r2a_version`** to a specific tag for reproducible deploys (e.g., `4.8.0` instead of `latest`) -- **Name files with numeric prefixes** (`10-dns.yaml`, `20-cdn.yaml`, `30-app.yaml`) for deterministic ordering -- **Use `#{Environment}` placeholders** in service IDs to keep configs environment-aware -- **Schedule nightly runs** (`cron: "0 2 * * *"`) to detect infrastructure drift -- **Keep `.rabbit/` configs small and focused** — one concern per file - ---- - ## License GPL-2.0 From 1fc8f7f3aba74fa0b61474ab02a247cbc1a3ca1b Mon Sep 17 00:00:00 2001 From: Andy Potanin Date: Sat, 2 May 2026 16:34:37 -0400 Subject: [PATCH 5/8] Bump actions/checkout to v5 and actions/upload-artifact to v5 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Node.js 20 actions are deprecated (EOL June 2, 2026). v5 of both actions uses Node.js 24. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- README.md | 6 +++--- action.yml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 13b0fdc..8bcafda 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 # Authenticate with your cloud provider(s) before calling the action - uses: google-github-actions/auth@v3 @@ -72,7 +72,7 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: aws-actions/configure-aws-credentials@v6 with: @@ -97,7 +97,7 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: azure/login@v2 with: diff --git a/action.yml b/action.yml index ba51bd4..267c03a 100644 --- a/action.yml +++ b/action.yml @@ -548,7 +548,7 @@ runs: - name: Upload terraform artifacts if: steps.safety.outputs.should_deploy == 'true' continue-on-error: true - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: terraform-artifacts-${{ steps.resolve-env.outputs.environment }} path: terraform/plans @@ -714,7 +714,7 @@ runs: - name: Upload terraform plans if: steps.plan-summary.outputs.has_changes == 'true' continue-on-error: true - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: terraform-plans-${{ steps.resolve-env.outputs.environment }} path: terraform/plans/*.tfplan From c8fc657fc344c694a4d4665a2c275b4c3bdeb6c7 Mon Sep 17 00:00:00 2001 From: Andy Potanin Date: Sat, 2 May 2026 17:34:39 -0400 Subject: [PATCH 6/8] Bump actions/upload-artifact from v5 to v6 for Node.js 24 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v5 still runs on Node.js 20. v6 is required for Node.js 24 runtime. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- action.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/action.yml b/action.yml index 267c03a..38c01cd 100644 --- a/action.yml +++ b/action.yml @@ -548,7 +548,7 @@ runs: - name: Upload terraform artifacts if: steps.safety.outputs.should_deploy == 'true' continue-on-error: true - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: terraform-artifacts-${{ steps.resolve-env.outputs.environment }} path: terraform/plans @@ -714,7 +714,7 @@ runs: - name: Upload terraform plans if: steps.plan-summary.outputs.has_changes == 'true' continue-on-error: true - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: terraform-plans-${{ steps.resolve-env.outputs.environment }} path: terraform/plans/*.tfplan From be4c709a388052496a01d9c83546104514a41cfd Mon Sep 17 00:00:00 2001 From: Andy Potanin Date: Sat, 2 May 2026 19:01:30 -0400 Subject: [PATCH 7/8] Address PR review: add state_backend default, guard AWS vars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add explicit default: "gcs" to state_backend input - Add :- fallback on AWS_SECRET_ACCESS_KEY to prevent unbound var error - Clarify README state_backend default column 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- README.md | 2 +- action.yml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8bcafda..c30cc94 100644 --- a/README.md +++ b/README.md @@ -399,7 +399,7 @@ The action automatically resolves `gcp://` prefixed values to actual secret valu | `newrelic_account_id` | — | — | New Relic account ID | | `newrelic_api_key` | — | — | New Relic API key | | `slack_webhook` | — | — | Slack webhook URL | -| `state_backend` | — | `gcs` | Backend type (`s3`, `azurerm`, `http`, `consul`) | +| `state_backend` | — | `gcs` | Backend type — defaults to GCS when unset (`s3`, `azurerm`, `http`, `consul`) | | `state_backend_config` | — | — | Backend config as key=value lines | | `state_prefix_key` | — | `prefix` | Backend key for state path | | `source_dir` | — | `.rabbit` | Config source directory | diff --git a/action.yml b/action.yml index 38c01cd..4a826fd 100644 --- a/action.yml +++ b/action.yml @@ -72,6 +72,7 @@ inputs: state_backend: description: "Override state backend type (e.g. s3, http, consul, azurerm). Unset = default gcs." required: false + default: "gcs" state_backend_config: description: "Backend config as key=value lines (e.g. bucket=my-bucket newline region=us-east-1). Required when state_backend is set." required: false @@ -466,7 +467,7 @@ runs: aws_env=() if [[ -n "${AWS_ACCESS_KEY_ID:-}" ]]; then aws_env+=(-e "AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}") - aws_env+=(-e "AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}") + aws_env+=(-e "AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-}") aws_env+=(-e "AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN:-}") aws_env+=(-e "AWS_REGION=${AWS_REGION:-}") fi From b35304e53d9dbf38eab6d13f7cedde0a0bfa24d2 Mon Sep 17 00:00:00 2001 From: Andy Potanin Date: Wed, 13 May 2026 14:03:39 -0400 Subject: [PATCH 8/8] Fix fabricated placeholders and secret syntax in README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Placeholders corrected to match actual registry in placeholders.sh: - #{Project} (not #{GcpProject}), #{Owner} (not #{GitOwner}), #{Repository} (not #{GitRepository}) - Removed non-existent #{Namespace} and #{SharedProject} Secret syntax: bare projects/{id}/secrets/{name} path, no gcp:// prefix 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- README.md | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index c30cc94..a7ad981 100644 --- a/README.md +++ b/README.md @@ -355,13 +355,11 @@ Use `#{Variable}` syntax in YAML values — they're replaced at runtime: | Placeholder | Value | | --- | --- | -| `#{Environment}` | Resolved environment name | -| `#{Lifecycle}` | Resolved lifecycle (production/staging/development) | -| `#{GcpProject}` | GCP project ID | -| `#{GitOwner}` | GitHub repository owner | -| `#{GitRepository}` | GitHub repository name | -| `#{Namespace}` | Kubernetes namespace (derived from repo name) | -| `#{SharedProject}` | Shared GCP project ID | +| `#{Environment}` | Environment name (from `$ENV_NAME`) | +| `#{Lifecycle}` | Lifecycle stage: production, staging, or development (from `$LIFECYCLE`) | +| `#{Project}` | GCP project ID (from `$GCP_PROJECT`) | +| `#{Owner}` | GitHub repository owner (from `$GIT_OWNER`) | +| `#{Repository}` | GitHub repository name (from `$GIT_REPOSITORY`) | ### GCP Secret Manager References @@ -373,10 +371,10 @@ services: id: app-secrets configurations: data: - DATABASE_URL: gcp://projects/my-project/secrets/db-url/versions/latest + DATABASE_URL: projects/123456789/secrets/db-url/versions/latest ``` -The action automatically resolves `gcp://` prefixed values to actual secret values at deploy time (requires GCP auth). +The action automatically resolves values matching the `projects/{id}/secrets/{name}` pattern to actual secret values at deploy time (requires `roles/secretmanager.secretAccessor` on the service account). ---