mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 12:04:02 +00:00
Merge pull request #4 from whekin/codex/whe-28-terraform
feat(WHE-28): Terraform baseline for Cloud Run, Scheduler, and Secrets
This commit is contained in:
22
.github/workflows/ci.yml
vendored
22
.github/workflows/ci.yml
vendored
@@ -79,3 +79,25 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
terraform:
|
||||||
|
name: Terraform / validate
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Terraform
|
||||||
|
uses: hashicorp/setup-terraform@v3
|
||||||
|
with:
|
||||||
|
terraform_version: 1.8.5
|
||||||
|
|
||||||
|
- name: Terraform format check
|
||||||
|
run: terraform -chdir=infra/terraform fmt -check -recursive
|
||||||
|
|
||||||
|
- name: Terraform validate
|
||||||
|
run: |
|
||||||
|
terraform -chdir=infra/terraform init -backend=false
|
||||||
|
terraform -chdir=infra/terraform validate
|
||||||
|
|||||||
11
AGENTS.md
11
AGENTS.md
@@ -1,9 +1,11 @@
|
|||||||
# AGENTS.md
|
# AGENTS.md
|
||||||
|
|
||||||
## Project
|
## Project
|
||||||
|
|
||||||
Household Telegram bot + mini app monorepo for shared rent/utilities/purchase accounting.
|
Household Telegram bot + mini app monorepo for shared rent/utilities/purchase accounting.
|
||||||
|
|
||||||
## Core stack
|
## Core stack
|
||||||
|
|
||||||
- Runtime/tooling: Bun
|
- Runtime/tooling: Bun
|
||||||
- Language: TypeScript (strict)
|
- Language: TypeScript (strict)
|
||||||
- Typecheck: tsgo (`@typescript/native-preview`)
|
- Typecheck: tsgo (`@typescript/native-preview`)
|
||||||
@@ -15,24 +17,29 @@ Household Telegram bot + mini app monorepo for shared rent/utilities/purchase ac
|
|||||||
- Deploy: Cloud Run + Cloud Scheduler (planned)
|
- Deploy: Cloud Run + Cloud Scheduler (planned)
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
Hexagonal architecture:
|
Hexagonal architecture:
|
||||||
|
|
||||||
- `packages/domain`: pure business rules/value objects only
|
- `packages/domain`: pure business rules/value objects only
|
||||||
- `packages/application`: use-cases only
|
- `packages/application`: use-cases only
|
||||||
- `packages/ports`: interfaces for external boundaries
|
- `packages/ports`: interfaces for external boundaries
|
||||||
- `apps/*`: composition/wiring and delivery endpoints
|
- `apps/*`: composition/wiring and delivery endpoints
|
||||||
|
|
||||||
Boundary rules:
|
Boundary rules:
|
||||||
|
|
||||||
- No framework/DB/HTTP imports in `packages/domain`
|
- No framework/DB/HTTP imports in `packages/domain`
|
||||||
- `packages/application` must not import adapter implementations
|
- `packages/application` must not import adapter implementations
|
||||||
- External SDK usage belongs outside domain/application core
|
- External SDK usage belongs outside domain/application core
|
||||||
|
|
||||||
## Money and accounting rules
|
## Money and accounting rules
|
||||||
|
|
||||||
- Never use floating-point money math
|
- Never use floating-point money math
|
||||||
- Store amounts in minor units
|
- Store amounts in minor units
|
||||||
- Deterministic split behavior only
|
- Deterministic split behavior only
|
||||||
- Persist raw parsed purchase text + confidence + parser mode
|
- Persist raw parsed purchase text + confidence + parser mode
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
- Work from Linear tickets and linked specs in `docs/specs/`
|
- Work from Linear tickets and linked specs in `docs/specs/`
|
||||||
- One ticket at a time, small commits
|
- One ticket at a time, small commits
|
||||||
- Before implementation: re-check ticket/spec and assumptions
|
- Before implementation: re-check ticket/spec and assumptions
|
||||||
@@ -40,7 +47,9 @@ Boundary rules:
|
|||||||
- Run CodeRabbit review before merge (`bun run review:coderabbit`)
|
- Run CodeRabbit review before merge (`bun run review:coderabbit`)
|
||||||
|
|
||||||
## Quality gates
|
## Quality gates
|
||||||
|
|
||||||
Required before PR/merge:
|
Required before PR/merge:
|
||||||
|
|
||||||
- `bun run format:check`
|
- `bun run format:check`
|
||||||
- `bun run lint`
|
- `bun run lint`
|
||||||
- `bun run typecheck`
|
- `bun run typecheck`
|
||||||
@@ -48,6 +57,7 @@ Required before PR/merge:
|
|||||||
- `bun run build`
|
- `bun run build`
|
||||||
|
|
||||||
## CI/CD
|
## CI/CD
|
||||||
|
|
||||||
- CI workflow runs parallel quality jobs on push/PR to `main`
|
- CI workflow runs parallel quality jobs on push/PR to `main`
|
||||||
- CD workflow deploys on successful `main` CI or manual trigger
|
- CD workflow deploys on successful `main` CI or manual trigger
|
||||||
- Required CD secrets:
|
- Required CD secrets:
|
||||||
@@ -56,6 +66,7 @@ Required before PR/merge:
|
|||||||
- `GCP_SERVICE_ACCOUNT`
|
- `GCP_SERVICE_ACCOUNT`
|
||||||
|
|
||||||
## Docs as source of truth
|
## Docs as source of truth
|
||||||
|
|
||||||
- Roadmap: `docs/roadmap.md`
|
- Roadmap: `docs/roadmap.md`
|
||||||
- Specs template: `docs/specs/README.md`
|
- Specs template: `docs/specs/README.md`
|
||||||
- ADRs: `docs/decisions/*`
|
- ADRs: `docs/decisions/*`
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
- Bun 1.3+
|
- Bun 1.3+
|
||||||
- Node.js 22+
|
- Node.js 22+
|
||||||
|
- Terraform 1.8+ (for IaC checks/plans)
|
||||||
|
|
||||||
## First-time setup
|
## First-time setup
|
||||||
|
|
||||||
@@ -24,6 +25,8 @@ bun run build
|
|||||||
bun run db:generate
|
bun run db:generate
|
||||||
bun run db:check
|
bun run db:check
|
||||||
bun run db:migrate
|
bun run db:migrate
|
||||||
|
bun run infra:fmt:check
|
||||||
|
bun run infra:validate
|
||||||
```
|
```
|
||||||
|
|
||||||
## App commands
|
## App commands
|
||||||
@@ -54,9 +57,14 @@ bun run review:coderabbit
|
|||||||
|
|
||||||
- CI runs in parallel matrix jobs on push/PR to `main`:
|
- CI runs in parallel matrix jobs on push/PR to `main`:
|
||||||
- `format:check`, `lint`, `typecheck`, `test`, `build`
|
- `format:check`, `lint`, `typecheck`, `test`, `build`
|
||||||
|
- `terraform fmt -check`, `terraform validate`
|
||||||
- CD deploys on successful `main` CI completion (or manual dispatch).
|
- CD deploys on successful `main` CI completion (or manual dispatch).
|
||||||
- CD is enabled when GitHub secrets are configured:
|
- CD is enabled when GitHub secrets are configured:
|
||||||
- `GCP_PROJECT_ID`
|
- `GCP_PROJECT_ID`
|
||||||
- `GCP_WORKLOAD_IDENTITY_PROVIDER`
|
- `GCP_WORKLOAD_IDENTITY_PROVIDER`
|
||||||
- `GCP_SERVICE_ACCOUNT`
|
- `GCP_SERVICE_ACCOUNT`
|
||||||
- optional for automated migrations: `DATABASE_URL`
|
- optional for automated migrations: `DATABASE_URL`
|
||||||
|
|
||||||
|
## IaC Runbook
|
||||||
|
|
||||||
|
- See `docs/runbooks/iac-terraform.md` for provisioning flow.
|
||||||
|
|||||||
55
docs/runbooks/iac-terraform.md
Normal file
55
docs/runbooks/iac-terraform.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# Terraform IaC Runbook
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Provision and maintain GCP infrastructure for bot API, mini app, scheduler, and runtime secrets.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Terraform `>= 1.8`
|
||||||
|
- GCP project with billing enabled
|
||||||
|
- Local auth:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gcloud auth application-default login
|
||||||
|
```
|
||||||
|
|
||||||
|
## Bootstrap
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp infra/terraform/terraform.tfvars.example infra/terraform/terraform.tfvars
|
||||||
|
terraform -chdir=infra/terraform init
|
||||||
|
terraform -chdir=infra/terraform plan
|
||||||
|
terraform -chdir=infra/terraform apply
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quality checks
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run infra:fmt:check
|
||||||
|
bun run infra:validate
|
||||||
|
```
|
||||||
|
|
||||||
|
## Add secret values
|
||||||
|
|
||||||
|
After first apply, add secret versions:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo -n "<telegram-webhook-secret>" | gcloud secrets versions add telegram-webhook-secret --data-file=- --project <project_id>
|
||||||
|
echo -n "<scheduler-shared-secret>" | gcloud secrets versions add scheduler-shared-secret --data-file=- --project <project_id>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment strategy
|
||||||
|
|
||||||
|
- Keep separate states for `dev` and `prod`.
|
||||||
|
- Prefer separate GCP projects for stronger isolation.
|
||||||
|
- Keep environment-specific variables in dedicated `*.tfvars` files.
|
||||||
|
|
||||||
|
## Destructive operations
|
||||||
|
|
||||||
|
Review plan output before apply/destroy:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
terraform -chdir=infra/terraform plan -destroy
|
||||||
|
terraform -chdir=infra/terraform destroy
|
||||||
|
```
|
||||||
77
docs/specs/HOUSEBOT-007-terraform-iac-baseline.md
Normal file
77
docs/specs/HOUSEBOT-007-terraform-iac-baseline.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# HOUSEBOT-007 Terraform IaC Baseline
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Define a reproducible GCP infrastructure baseline for deployment of the bot API and mini app, including scheduling and secrets.
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
- Provision Cloud Run services for bot API and mini app.
|
||||||
|
- Provision Cloud Scheduler reminder trigger.
|
||||||
|
- Provision Secret Manager placeholders and runtime access bindings.
|
||||||
|
- Provision Artifact Registry repository for container images.
|
||||||
|
- Provide optional GitHub OIDC Workload Identity resources.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Business feature implementation.
|
||||||
|
- Full observability stack (Grafana/Prometheus) in this ticket.
|
||||||
|
- Multi-region failover.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- In: Terraform scaffold, docs, CI validation.
|
||||||
|
- Out: runtime deploy script rewrites, production dashboard configuration.
|
||||||
|
|
||||||
|
## Interfaces and Contracts
|
||||||
|
|
||||||
|
- Scheduler sends HTTP request to `POST /internal/scheduler/reminders`.
|
||||||
|
- Bot runtime reads secret-backed env vars:
|
||||||
|
- `TELEGRAM_WEBHOOK_SECRET`
|
||||||
|
- `SCHEDULER_SHARED_SECRET`
|
||||||
|
- `SUPABASE_URL` (optional)
|
||||||
|
- `SUPABASE_PUBLISHABLE_KEY` (optional)
|
||||||
|
|
||||||
|
## Domain Rules
|
||||||
|
|
||||||
|
- N/A (infrastructure-only change).
|
||||||
|
|
||||||
|
## Data Model Changes
|
||||||
|
|
||||||
|
- None.
|
||||||
|
|
||||||
|
## Security and Privacy
|
||||||
|
|
||||||
|
- Runtime access to secrets is explicit via `roles/secretmanager.secretAccessor`.
|
||||||
|
- Scheduler uses OIDC token with dedicated service account.
|
||||||
|
- GitHub OIDC setup is optional and repository-scoped.
|
||||||
|
|
||||||
|
## Observability
|
||||||
|
|
||||||
|
- Out of scope for this ticket.
|
||||||
|
|
||||||
|
## Edge Cases and Failure Modes
|
||||||
|
|
||||||
|
- Missing secret versions causes runtime startup/read failures.
|
||||||
|
- Scheduler remains paused by default to avoid accidental reminders.
|
||||||
|
- Incorrect `bot_api_image` or `mini_app_image` tags causes deployment failures.
|
||||||
|
|
||||||
|
## Test Plan
|
||||||
|
|
||||||
|
- Unit: N/A
|
||||||
|
- Integration: `terraform validate`
|
||||||
|
- E2E: Apply in dev project and verify service URLs + scheduler job presence.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] `terraform plan` succeeds with provided vars.
|
||||||
|
- [ ] Two Cloud Run services and one Scheduler job are provisioned.
|
||||||
|
- [ ] Runtime secret access is bound explicitly.
|
||||||
|
- [ ] CI validates Terraform formatting and configuration.
|
||||||
|
- [ ] Runbook documents local and CI workflow.
|
||||||
|
|
||||||
|
## Rollout Plan
|
||||||
|
|
||||||
|
- Apply to dev first with scheduler paused.
|
||||||
|
- Add secret versions.
|
||||||
|
- Unpause scheduler after reminder endpoint is implemented and verified.
|
||||||
4
infra/terraform/.gitignore
vendored
Normal file
4
infra/terraform/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.terraform
|
||||||
|
*.tfstate
|
||||||
|
*.tfstate.*
|
||||||
|
terraform.tfvars
|
||||||
22
infra/terraform/.terraform.lock.hcl
generated
Normal file
22
infra/terraform/.terraform.lock.hcl
generated
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# This file is maintained automatically by "terraform init".
|
||||||
|
# Manual edits may be lost in future updates.
|
||||||
|
|
||||||
|
provider "registry.terraform.io/hashicorp/google" {
|
||||||
|
version = "6.50.0"
|
||||||
|
constraints = "~> 6.0"
|
||||||
|
hashes = [
|
||||||
|
"h1:79CwMTsp3Ud1nOl5hFS5mxQHyT0fGVye7pqpU0PPlHI=",
|
||||||
|
"zh:1f3513fcfcbf7ca53d667a168c5067a4dd91a4d4cccd19743e248ff31065503c",
|
||||||
|
"zh:3da7db8fc2c51a77dd958ea8baaa05c29cd7f829bd8941c26e2ea9cb3aadc1e5",
|
||||||
|
"zh:3e09ac3f6ca8111cbb659d38c251771829f4347ab159a12db195e211c76068bb",
|
||||||
|
"zh:7bb9e41c568df15ccf1a8946037355eefb4dfb4e35e3b190808bb7c4abae547d",
|
||||||
|
"zh:81e5d78bdec7778e6d67b5c3544777505db40a826b6eb5abe9b86d4ba396866b",
|
||||||
|
"zh:8d309d020fb321525883f5c4ea864df3d5942b6087f6656d6d8b3a1377f340fc",
|
||||||
|
"zh:93e112559655ab95a523193158f4a4ac0f2bfed7eeaa712010b85ebb551d5071",
|
||||||
|
"zh:d3efe589ffd625b300cef5917c4629513f77e3a7b111c9df65075f76a46a63c7",
|
||||||
|
"zh:d4a4d672bbef756a870d8f32b35925f8ce2ef4f6bbd5b71a3cb764f1b6c85421",
|
||||||
|
"zh:e13a86bca299ba8a118e80d5f84fbdd708fe600ecdceea1a13d4919c068379fe",
|
||||||
|
"zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c",
|
||||||
|
"zh:fec30c095647b583a246c39d557704947195a1b7d41f81e369ba377d997faef6",
|
||||||
|
]
|
||||||
|
}
|
||||||
79
infra/terraform/README.md
Normal file
79
infra/terraform/README.md
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# Terraform Infrastructure (WHE-28)
|
||||||
|
|
||||||
|
This directory contains baseline IaC for deploying the household bot platform on GCP.
|
||||||
|
|
||||||
|
## Provisioned resources
|
||||||
|
|
||||||
|
- Artifact Registry Docker repository
|
||||||
|
- Cloud Run service: bot API (public webhook endpoint)
|
||||||
|
- Cloud Run service: mini app (public web UI)
|
||||||
|
- Cloud Scheduler job for reminder triggers
|
||||||
|
- Runtime and scheduler service accounts with least-privilege bindings
|
||||||
|
- Secret Manager secrets (IDs only, secret values are added separately)
|
||||||
|
- Optional GitHub OIDC Workload Identity setup for deploy automation
|
||||||
|
|
||||||
|
## Architecture (v1)
|
||||||
|
|
||||||
|
- `bot-api`: Telegram webhook + app API endpoints
|
||||||
|
- `mini-app`: front-end delivery
|
||||||
|
- `scheduler`: triggers `bot-api` internal reminder endpoint using OIDC token
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Terraform `>= 1.8`
|
||||||
|
- Authenticated GCP CLI context (`gcloud auth application-default login` for local)
|
||||||
|
- Enabled billing on the target GCP project
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Initialize:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
terraform -chdir=infra/terraform init
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Prepare variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp infra/terraform/terraform.tfvars.example infra/terraform/terraform.tfvars
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Plan:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
terraform -chdir=infra/terraform plan
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Apply:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
terraform -chdir=infra/terraform apply
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Add secret values (after apply):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo -n "<value>" | gcloud secrets versions add telegram-webhook-secret --data-file=- --project <project_id>
|
||||||
|
echo -n "<value>" | gcloud secrets versions add scheduler-shared-secret --data-file=- --project <project_id>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environments
|
||||||
|
|
||||||
|
Recommended approach:
|
||||||
|
|
||||||
|
- Keep one state per environment (dev/prod) using separate backend configs or workspaces
|
||||||
|
- Use `terraform.tfvars` per environment (`dev.tfvars`, `prod.tfvars`)
|
||||||
|
- Keep `project_id` separate for dev/prod when possible
|
||||||
|
|
||||||
|
## CI validation
|
||||||
|
|
||||||
|
CI runs:
|
||||||
|
|
||||||
|
- `terraform -chdir=infra/terraform fmt -check -recursive`
|
||||||
|
- `terraform -chdir=infra/terraform init -backend=false`
|
||||||
|
- `terraform -chdir=infra/terraform validate`
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Scheduler job defaults to `paused = true` to prevent accidental sends before app logic is ready.
|
||||||
|
- Bot API is public to accept Telegram webhooks; scheduler endpoint should still verify app-level auth.
|
||||||
37
infra/terraform/locals.tf
Normal file
37
infra/terraform/locals.tf
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
locals {
|
||||||
|
name_prefix = "${var.service_prefix}-${var.environment}"
|
||||||
|
|
||||||
|
common_labels = merge(
|
||||||
|
{
|
||||||
|
environment = var.environment
|
||||||
|
managed_by = "terraform"
|
||||||
|
project = "household-bot"
|
||||||
|
},
|
||||||
|
var.labels
|
||||||
|
)
|
||||||
|
|
||||||
|
artifact_location = coalesce(var.artifact_repository_location, var.region)
|
||||||
|
|
||||||
|
runtime_secret_ids = toset(compact([
|
||||||
|
var.telegram_webhook_secret_id,
|
||||||
|
var.scheduler_shared_secret_id,
|
||||||
|
var.supabase_url_secret_id,
|
||||||
|
var.supabase_publishable_key_secret_id
|
||||||
|
]))
|
||||||
|
|
||||||
|
api_services = toset([
|
||||||
|
"artifactregistry.googleapis.com",
|
||||||
|
"cloudscheduler.googleapis.com",
|
||||||
|
"iam.googleapis.com",
|
||||||
|
"iamcredentials.googleapis.com",
|
||||||
|
"run.googleapis.com",
|
||||||
|
"secretmanager.googleapis.com",
|
||||||
|
"sts.googleapis.com"
|
||||||
|
])
|
||||||
|
|
||||||
|
github_deploy_roles = toset([
|
||||||
|
"roles/artifactregistry.writer",
|
||||||
|
"roles/iam.serviceAccountUser",
|
||||||
|
"roles/run.admin"
|
||||||
|
])
|
||||||
|
}
|
||||||
218
infra/terraform/main.tf
Normal file
218
infra/terraform/main.tf
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
data "google_project" "current" {
|
||||||
|
project_id = var.project_id
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_project_service" "enabled" {
|
||||||
|
for_each = local.api_services
|
||||||
|
project = var.project_id
|
||||||
|
service = each.value
|
||||||
|
disable_on_destroy = false
|
||||||
|
disable_dependent_services = false
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_artifact_registry_repository" "containers" {
|
||||||
|
location = local.artifact_location
|
||||||
|
project = var.project_id
|
||||||
|
repository_id = var.artifact_repository_id
|
||||||
|
description = "Container images for household bot"
|
||||||
|
format = "DOCKER"
|
||||||
|
|
||||||
|
labels = local.common_labels
|
||||||
|
|
||||||
|
depends_on = [google_project_service.enabled]
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_service_account" "bot_runtime" {
|
||||||
|
project = var.project_id
|
||||||
|
account_id = "${var.environment}-bot-runtime"
|
||||||
|
display_name = "${local.name_prefix} bot runtime"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_service_account" "mini_runtime" {
|
||||||
|
project = var.project_id
|
||||||
|
account_id = "${var.environment}-mini-runtime"
|
||||||
|
display_name = "${local.name_prefix} mini runtime"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_service_account" "scheduler_invoker" {
|
||||||
|
project = var.project_id
|
||||||
|
account_id = "${var.environment}-scheduler"
|
||||||
|
display_name = "${local.name_prefix} scheduler invoker"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_secret_manager_secret" "runtime" {
|
||||||
|
for_each = local.runtime_secret_ids
|
||||||
|
|
||||||
|
project = var.project_id
|
||||||
|
secret_id = each.value
|
||||||
|
|
||||||
|
replication {
|
||||||
|
auto {}
|
||||||
|
}
|
||||||
|
|
||||||
|
labels = local.common_labels
|
||||||
|
|
||||||
|
depends_on = [google_project_service.enabled]
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_secret_manager_secret_iam_member" "bot_runtime_access" {
|
||||||
|
for_each = google_secret_manager_secret.runtime
|
||||||
|
|
||||||
|
project = var.project_id
|
||||||
|
secret_id = each.value.secret_id
|
||||||
|
role = "roles/secretmanager.secretAccessor"
|
||||||
|
member = "serviceAccount:${google_service_account.bot_runtime.email}"
|
||||||
|
}
|
||||||
|
|
||||||
|
module "bot_api_service" {
|
||||||
|
source = "./modules/cloud_run_service"
|
||||||
|
|
||||||
|
project_id = var.project_id
|
||||||
|
region = var.region
|
||||||
|
name = "${local.name_prefix}-bot-api"
|
||||||
|
service_account_email = google_service_account.bot_runtime.email
|
||||||
|
image = var.bot_api_image
|
||||||
|
allow_unauthenticated = true
|
||||||
|
min_instance_count = var.bot_min_instances
|
||||||
|
max_instance_count = var.bot_max_instances
|
||||||
|
labels = local.common_labels
|
||||||
|
|
||||||
|
env = {
|
||||||
|
NODE_ENV = var.environment
|
||||||
|
}
|
||||||
|
|
||||||
|
secret_env = merge(
|
||||||
|
{
|
||||||
|
TELEGRAM_WEBHOOK_SECRET = var.telegram_webhook_secret_id
|
||||||
|
SCHEDULER_SHARED_SECRET = var.scheduler_shared_secret_id
|
||||||
|
},
|
||||||
|
var.supabase_url_secret_id == null ? {} : {
|
||||||
|
SUPABASE_URL = var.supabase_url_secret_id
|
||||||
|
},
|
||||||
|
var.supabase_publishable_key_secret_id == null ? {} : {
|
||||||
|
SUPABASE_PUBLISHABLE_KEY = var.supabase_publishable_key_secret_id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
depends_on = [
|
||||||
|
google_project_service.enabled,
|
||||||
|
google_secret_manager_secret.runtime
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
module "mini_app_service" {
|
||||||
|
source = "./modules/cloud_run_service"
|
||||||
|
|
||||||
|
project_id = var.project_id
|
||||||
|
region = var.region
|
||||||
|
name = "${local.name_prefix}-mini-app"
|
||||||
|
service_account_email = google_service_account.mini_runtime.email
|
||||||
|
image = var.mini_app_image
|
||||||
|
allow_unauthenticated = true
|
||||||
|
min_instance_count = var.mini_min_instances
|
||||||
|
max_instance_count = var.mini_max_instances
|
||||||
|
labels = local.common_labels
|
||||||
|
|
||||||
|
env = {
|
||||||
|
NODE_ENV = var.environment
|
||||||
|
}
|
||||||
|
|
||||||
|
depends_on = [google_project_service.enabled]
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_cloud_run_v2_service_iam_member" "scheduler_invoker" {
|
||||||
|
project = var.project_id
|
||||||
|
location = var.region
|
||||||
|
name = module.bot_api_service.name
|
||||||
|
role = "roles/run.invoker"
|
||||||
|
member = "serviceAccount:${google_service_account.scheduler_invoker.email}"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_service_account_iam_member" "scheduler_token_creator" {
|
||||||
|
service_account_id = google_service_account.scheduler_invoker.name
|
||||||
|
role = "roles/iam.serviceAccountTokenCreator"
|
||||||
|
member = "serviceAccount:service-${data.google_project.current.number}@gcp-sa-cloudscheduler.iam.gserviceaccount.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_cloud_scheduler_job" "reminders" {
|
||||||
|
project = var.project_id
|
||||||
|
region = var.region
|
||||||
|
name = "${local.name_prefix}-reminders"
|
||||||
|
schedule = var.scheduler_cron
|
||||||
|
time_zone = var.scheduler_timezone
|
||||||
|
paused = var.scheduler_paused
|
||||||
|
|
||||||
|
http_target {
|
||||||
|
uri = "${module.bot_api_service.uri}${var.scheduler_path}"
|
||||||
|
http_method = var.scheduler_http_method
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Content-Type" = "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
body = base64encode(var.scheduler_body_json)
|
||||||
|
|
||||||
|
oidc_token {
|
||||||
|
service_account_email = google_service_account.scheduler_invoker.email
|
||||||
|
audience = module.bot_api_service.uri
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
depends_on = [
|
||||||
|
module.bot_api_service,
|
||||||
|
google_service_account_iam_member.scheduler_token_creator
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_service_account" "github_deployer" {
|
||||||
|
count = var.create_workload_identity ? 1 : 0
|
||||||
|
|
||||||
|
project = var.project_id
|
||||||
|
account_id = var.github_deploy_service_account_id
|
||||||
|
display_name = "${local.name_prefix} GitHub deployer"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_iam_workload_identity_pool" "github" {
|
||||||
|
count = var.create_workload_identity ? 1 : 0
|
||||||
|
|
||||||
|
project = var.project_id
|
||||||
|
workload_identity_pool_id = var.workload_identity_pool_id
|
||||||
|
display_name = "GitHub Actions Pool"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_iam_workload_identity_pool_provider" "github" {
|
||||||
|
count = var.create_workload_identity ? 1 : 0
|
||||||
|
|
||||||
|
project = var.project_id
|
||||||
|
workload_identity_pool_id = google_iam_workload_identity_pool.github[0].workload_identity_pool_id
|
||||||
|
workload_identity_pool_provider_id = var.workload_identity_provider_id
|
||||||
|
display_name = "GitHub Actions Provider"
|
||||||
|
|
||||||
|
attribute_mapping = {
|
||||||
|
"google.subject" = "assertion.sub"
|
||||||
|
"attribute.actor" = "assertion.actor"
|
||||||
|
"attribute.repository" = "assertion.repository"
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute_condition = "assertion.repository == \"${var.github_repository}\""
|
||||||
|
|
||||||
|
oidc {
|
||||||
|
issuer_uri = "https://token.actions.githubusercontent.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_service_account_iam_member" "github_oidc" {
|
||||||
|
count = var.create_workload_identity ? 1 : 0
|
||||||
|
|
||||||
|
service_account_id = google_service_account.github_deployer[0].name
|
||||||
|
role = "roles/iam.workloadIdentityUser"
|
||||||
|
member = "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.github[0].name}/attribute.repository/${var.github_repository}"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_project_iam_member" "github_deployer_roles" {
|
||||||
|
for_each = var.create_workload_identity ? local.github_deploy_roles : toset([])
|
||||||
|
|
||||||
|
project = var.project_id
|
||||||
|
role = each.value
|
||||||
|
member = "serviceAccount:${google_service_account.github_deployer[0].email}"
|
||||||
|
}
|
||||||
67
infra/terraform/modules/cloud_run_service/main.tf
Normal file
67
infra/terraform/modules/cloud_run_service/main.tf
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
resource "google_cloud_run_v2_service" "this" {
|
||||||
|
project = var.project_id
|
||||||
|
location = var.region
|
||||||
|
name = var.name
|
||||||
|
|
||||||
|
ingress = "INGRESS_TRAFFIC_ALL"
|
||||||
|
deletion_protection = false
|
||||||
|
|
||||||
|
labels = var.labels
|
||||||
|
|
||||||
|
template {
|
||||||
|
service_account = var.service_account_email
|
||||||
|
|
||||||
|
scaling {
|
||||||
|
min_instance_count = var.min_instance_count
|
||||||
|
max_instance_count = var.max_instance_count
|
||||||
|
}
|
||||||
|
|
||||||
|
containers {
|
||||||
|
image = var.image
|
||||||
|
|
||||||
|
ports {
|
||||||
|
container_port = var.container_port
|
||||||
|
}
|
||||||
|
|
||||||
|
resources {
|
||||||
|
limits = var.limits
|
||||||
|
}
|
||||||
|
|
||||||
|
dynamic "env" {
|
||||||
|
for_each = var.env
|
||||||
|
content {
|
||||||
|
name = env.key
|
||||||
|
value = env.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dynamic "env" {
|
||||||
|
for_each = var.secret_env
|
||||||
|
content {
|
||||||
|
name = env.key
|
||||||
|
value_source {
|
||||||
|
secret_key_ref {
|
||||||
|
secret = env.value
|
||||||
|
version = "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
traffic {
|
||||||
|
percent = 100
|
||||||
|
type = "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_cloud_run_v2_service_iam_member" "public_invoker" {
|
||||||
|
count = var.allow_unauthenticated ? 1 : 0
|
||||||
|
|
||||||
|
project = var.project_id
|
||||||
|
location = var.region
|
||||||
|
name = google_cloud_run_v2_service.this.name
|
||||||
|
role = "roles/run.invoker"
|
||||||
|
member = "allUsers"
|
||||||
|
}
|
||||||
7
infra/terraform/modules/cloud_run_service/outputs.tf
Normal file
7
infra/terraform/modules/cloud_run_service/outputs.tf
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
output "name" {
|
||||||
|
value = google_cloud_run_v2_service.this.name
|
||||||
|
}
|
||||||
|
|
||||||
|
output "uri" {
|
||||||
|
value = google_cloud_run_v2_service.this.uri
|
||||||
|
}
|
||||||
63
infra/terraform/modules/cloud_run_service/variables.tf
Normal file
63
infra/terraform/modules/cloud_run_service/variables.tf
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
variable "project_id" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "region" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "name" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "image" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "service_account_email" {
|
||||||
|
type = string
|
||||||
|
default = null
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "allow_unauthenticated" {
|
||||||
|
type = bool
|
||||||
|
default = false
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "env" {
|
||||||
|
type = map(string)
|
||||||
|
default = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "secret_env" {
|
||||||
|
type = map(string)
|
||||||
|
default = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "labels" {
|
||||||
|
type = map(string)
|
||||||
|
default = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "container_port" {
|
||||||
|
type = number
|
||||||
|
default = 8080
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "min_instance_count" {
|
||||||
|
type = number
|
||||||
|
default = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "max_instance_count" {
|
||||||
|
type = number
|
||||||
|
default = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "limits" {
|
||||||
|
type = map(string)
|
||||||
|
default = {
|
||||||
|
cpu = "1"
|
||||||
|
memory = "512Mi"
|
||||||
|
}
|
||||||
|
}
|
||||||
44
infra/terraform/outputs.tf
Normal file
44
infra/terraform/outputs.tf
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
output "artifact_repository" {
|
||||||
|
description = "Artifact Registry repository"
|
||||||
|
value = google_artifact_registry_repository.containers.id
|
||||||
|
}
|
||||||
|
|
||||||
|
output "bot_api_service_name" {
|
||||||
|
description = "Cloud Run bot API service name"
|
||||||
|
value = module.bot_api_service.name
|
||||||
|
}
|
||||||
|
|
||||||
|
output "bot_api_service_url" {
|
||||||
|
description = "Cloud Run bot API URL"
|
||||||
|
value = module.bot_api_service.uri
|
||||||
|
}
|
||||||
|
|
||||||
|
output "mini_app_service_name" {
|
||||||
|
description = "Cloud Run mini app service name"
|
||||||
|
value = module.mini_app_service.name
|
||||||
|
}
|
||||||
|
|
||||||
|
output "mini_app_service_url" {
|
||||||
|
description = "Cloud Run mini app URL"
|
||||||
|
value = module.mini_app_service.uri
|
||||||
|
}
|
||||||
|
|
||||||
|
output "scheduler_job_name" {
|
||||||
|
description = "Cloud Scheduler job for reminders"
|
||||||
|
value = google_cloud_scheduler_job.reminders.name
|
||||||
|
}
|
||||||
|
|
||||||
|
output "runtime_secret_ids" {
|
||||||
|
description = "Secret Manager IDs expected by runtime"
|
||||||
|
value = sort([for secret in google_secret_manager_secret.runtime : secret.secret_id])
|
||||||
|
}
|
||||||
|
|
||||||
|
output "github_deployer_service_account" {
|
||||||
|
description = "GitHub OIDC deployer service account email"
|
||||||
|
value = var.create_workload_identity ? google_service_account.github_deployer[0].email : null
|
||||||
|
}
|
||||||
|
|
||||||
|
output "github_workload_identity_provider" {
|
||||||
|
description = "Full Workload Identity Provider resource name"
|
||||||
|
value = var.create_workload_identity ? google_iam_workload_identity_pool_provider.github[0].name : null
|
||||||
|
}
|
||||||
4
infra/terraform/providers.tf
Normal file
4
infra/terraform/providers.tf
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
provider "google" {
|
||||||
|
project = var.project_id
|
||||||
|
region = var.region
|
||||||
|
}
|
||||||
16
infra/terraform/terraform.tfvars.example
Normal file
16
infra/terraform/terraform.tfvars.example
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
project_id = "my-gcp-project"
|
||||||
|
region = "europe-west1"
|
||||||
|
environment = "dev"
|
||||||
|
service_prefix = "household"
|
||||||
|
|
||||||
|
artifact_repository_id = "household-bot"
|
||||||
|
|
||||||
|
bot_api_image = "europe-west1-docker.pkg.dev/my-gcp-project/household-bot/bot-api:latest"
|
||||||
|
mini_app_image = "europe-west1-docker.pkg.dev/my-gcp-project/household-bot/mini-app:latest"
|
||||||
|
|
||||||
|
scheduler_cron = "0 9 * * *"
|
||||||
|
scheduler_timezone = "Asia/Tbilisi"
|
||||||
|
scheduler_paused = true
|
||||||
|
|
||||||
|
create_workload_identity = true
|
||||||
|
github_repository = "whekin/household-bot"
|
||||||
167
infra/terraform/variables.tf
Normal file
167
infra/terraform/variables.tf
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
variable "project_id" {
|
||||||
|
description = "GCP project ID"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "region" {
|
||||||
|
description = "Primary GCP region for Cloud Run services"
|
||||||
|
type = string
|
||||||
|
default = "europe-west1"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "environment" {
|
||||||
|
description = "Environment name (e.g. dev, prod)"
|
||||||
|
type = string
|
||||||
|
default = "dev"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "service_prefix" {
|
||||||
|
description = "Prefix for service names"
|
||||||
|
type = string
|
||||||
|
default = "household"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "artifact_repository_id" {
|
||||||
|
description = "Artifact Registry repository ID"
|
||||||
|
type = string
|
||||||
|
default = "household-bot"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "artifact_repository_location" {
|
||||||
|
description = "Artifact Registry location (defaults to region)"
|
||||||
|
type = string
|
||||||
|
default = null
|
||||||
|
nullable = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "bot_api_image" {
|
||||||
|
description = "Container image for bot API service"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "mini_app_image" {
|
||||||
|
description = "Container image for mini app service"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "telegram_webhook_secret_id" {
|
||||||
|
description = "Secret Manager ID for Telegram webhook secret token"
|
||||||
|
type = string
|
||||||
|
default = "telegram-webhook-secret"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "scheduler_shared_secret_id" {
|
||||||
|
description = "Secret Manager ID for app-level scheduler secret"
|
||||||
|
type = string
|
||||||
|
default = "scheduler-shared-secret"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "supabase_url_secret_id" {
|
||||||
|
description = "Optional Secret Manager ID for SUPABASE_URL"
|
||||||
|
type = string
|
||||||
|
default = null
|
||||||
|
nullable = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "supabase_publishable_key_secret_id" {
|
||||||
|
description = "Optional Secret Manager ID for SUPABASE_PUBLISHABLE_KEY"
|
||||||
|
type = string
|
||||||
|
default = null
|
||||||
|
nullable = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "scheduler_path" {
|
||||||
|
description = "Reminder endpoint path on bot API"
|
||||||
|
type = string
|
||||||
|
default = "/internal/scheduler/reminders"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "scheduler_http_method" {
|
||||||
|
description = "Scheduler HTTP method"
|
||||||
|
type = string
|
||||||
|
default = "POST"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "scheduler_cron" {
|
||||||
|
description = "Cron expression for reminder scheduler"
|
||||||
|
type = string
|
||||||
|
default = "0 9 * * *"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "scheduler_timezone" {
|
||||||
|
description = "Scheduler timezone"
|
||||||
|
type = string
|
||||||
|
default = "Asia/Tbilisi"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "scheduler_body_json" {
|
||||||
|
description = "JSON payload for scheduler requests"
|
||||||
|
type = string
|
||||||
|
default = "{\"kind\":\"monthly-reminder\"}"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "scheduler_paused" {
|
||||||
|
description = "Whether scheduler should be paused initially"
|
||||||
|
type = bool
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "bot_min_instances" {
|
||||||
|
description = "Minimum bot API instances"
|
||||||
|
type = number
|
||||||
|
default = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "bot_max_instances" {
|
||||||
|
description = "Maximum bot API instances"
|
||||||
|
type = number
|
||||||
|
default = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "mini_min_instances" {
|
||||||
|
description = "Minimum mini app instances"
|
||||||
|
type = number
|
||||||
|
default = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "mini_max_instances" {
|
||||||
|
description = "Maximum mini app instances"
|
||||||
|
type = number
|
||||||
|
default = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "labels" {
|
||||||
|
description = "Additional labels"
|
||||||
|
type = map(string)
|
||||||
|
default = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "create_workload_identity" {
|
||||||
|
description = "Create GitHub OIDC Workload Identity resources"
|
||||||
|
type = bool
|
||||||
|
default = false
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "github_repository" {
|
||||||
|
description = "GitHub repository in owner/repo format"
|
||||||
|
type = string
|
||||||
|
default = "whekin/household-bot"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "workload_identity_pool_id" {
|
||||||
|
description = "Workload Identity Pool ID"
|
||||||
|
type = string
|
||||||
|
default = "github-pool"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "workload_identity_provider_id" {
|
||||||
|
description = "Workload Identity Provider ID"
|
||||||
|
type = string
|
||||||
|
default = "github-provider"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "github_deploy_service_account_id" {
|
||||||
|
description = "Service account ID used by GitHub Actions via OIDC"
|
||||||
|
type = string
|
||||||
|
default = "github-deployer"
|
||||||
|
}
|
||||||
10
infra/terraform/versions.tf
Normal file
10
infra/terraform/versions.tf
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
terraform {
|
||||||
|
required_version = ">= 1.8.0"
|
||||||
|
|
||||||
|
required_providers {
|
||||||
|
google = {
|
||||||
|
source = "hashicorp/google"
|
||||||
|
version = "~> 6.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,9 @@
|
|||||||
"db:push": "bunx drizzle-kit push --config packages/db/drizzle.config.ts",
|
"db:push": "bunx drizzle-kit push --config packages/db/drizzle.config.ts",
|
||||||
"db:studio": "bunx drizzle-kit studio --config packages/db/drizzle.config.ts",
|
"db:studio": "bunx drizzle-kit studio --config packages/db/drizzle.config.ts",
|
||||||
"review:coderabbit": "coderabbit --prompt-only --base main || ~/.local/bin/coderabbit --prompt-only --base main",
|
"review:coderabbit": "coderabbit --prompt-only --base main || ~/.local/bin/coderabbit --prompt-only --base main",
|
||||||
|
"infra:fmt": "terraform -chdir=infra/terraform fmt -recursive",
|
||||||
|
"infra:fmt:check": "terraform -chdir=infra/terraform fmt -check -recursive",
|
||||||
|
"infra:validate": "terraform -chdir=infra/terraform init -backend=false && terraform -chdir=infra/terraform validate",
|
||||||
"dev:bot": "bun run --filter @household/bot dev",
|
"dev:bot": "bun run --filter @household/bot dev",
|
||||||
"dev:miniapp": "bun run --filter @household/miniapp dev"
|
"dev:miniapp": "bun run --filter @household/miniapp dev"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user