diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c945621d..319251b6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,7 @@ concurrency: permissions: contents: read + id-token: write defaults: run: @@ -38,6 +39,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 + - uses: google-github-actions/auth@v3 + with: + project_id: swift2023groupc + workload_identity_provider: ${{ vars.GCP_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ vars.GCP_SERVICE_ACCOUNT }} - uses: ./.github/actions/setup-ci - run: mise test-unit @@ -47,6 +53,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 + - uses: google-github-actions/auth@v3 + with: + project_id: swift2023groupc + workload_identity_provider: ${{ vars.GCP_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ vars.GCP_SERVICE_ACCOUNT }} - uses: ./.github/actions/setup-ci - run: mise build - run: mise analyze diff --git a/.gitignore b/.gitignore index ebc6aef6..b4eb048a 100644 --- a/.gitignore +++ b/.gitignore @@ -108,9 +108,15 @@ Temporary Items **/**.log **/**.mocks.dart **/dart_defines/**.json +.terraform/ +*.tfstate +*.tfstate.* +*.tfvars +!*.tfvars.example .claude/worktrees/ .idea/ firebase.json firebase_options.dart +gha-creds-*.json google-services.json GoogleService-Info.plist diff --git a/infra/github-actions-wif/.terraform.lock.hcl b/infra/github-actions-wif/.terraform.lock.hcl new file mode 100644 index 00000000..e57267bb --- /dev/null +++ b/infra/github-actions-wif/.terraform.lock.hcl @@ -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 = "7.34.0" + constraints = "~> 7.0" + hashes = [ + "h1:9QgBib/ID/c8aDjHibC+ZowRVjb8+jP72cDoas+SpBc=", + "zh:0d824f222454358a9686ead2b75059777dcfd8006ed929c514ba3454d2cd91ef", + "zh:3e59be6923aeef1669c122711f62f9d01880308dfe08fc2817807309be46e417", + "zh:809458bac1ea6f372934d28295ecacffd0cd5b31df117b9bbeed2b91b0cd6b98", + "zh:ac7178ed164abfbb058e4439aaff7dd1b06f56194440c46a89c0915aef39a35a", + "zh:b4bef4c098a7223a7d56c33324f03fafe91eee8f5a2874bf64a6d3e87fc46613", + "zh:bf659617d992c93aa02cf909fa940e21f8b90e662876d38aa8cdb27eac857adc", + "zh:cbfa1b666caf1f4497ef95b9beff7ba1911ee042e6c0a3f19478b5ff450fb507", + "zh:dcacfa747673ee2cf0d6b84d5f155ab0702210146d76402ecef719a1a8c16c1e", + "zh:f15751387ef9844c91f2e37e82e8693758a4f7802defb1543a55713a62306875", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + "zh:f7009e2aa90b8637ffd6727ffb4358100389903c3510e4899f8ed6020c0ebb9e", + "zh:fcfa7e41fcaf19ee809d70d45222ce0e87782bac3515b5ca5ea4f8209d86cd04", + ] +} diff --git a/infra/github-actions-wif/README.md b/infra/github-actions-wif/README.md new file mode 100644 index 00000000..cac1c88d --- /dev/null +++ b/infra/github-actions-wif/README.md @@ -0,0 +1,67 @@ +# GitHub Actions Workload Identity Federation + +GitHub Actions から `flutterfire configure` を実行するための Google Cloud Workload Identity Federation 構成です。 + +## 管理するリソース + +- GitHub Actions 用 Service Account +- Workload Identity Pool +- GitHub OIDC Provider +- Service Account への `roles/iam.workloadIdentityUser` 付与 +- Service Account への Firebase 参照権限 + +## 初期設定 + +既存の `dotto` pool / `github` provider / `github-actions@swift2023groupc.iam.gserviceaccount.com` を引き継ぐため、初回は `apply` の前に import します。 + +```bash +cp infra/github-actions-wif/terraform.tfvars.example infra/github-actions-wif/terraform.tfvars +mise exec terraform -- terraform -chdir=infra/github-actions-wif init +``` + +```bash +terraform -chdir=infra/github-actions-wif import \ + google_service_account.github_actions \ + projects/swift2023groupc/serviceAccounts/github-actions@swift2023groupc.iam.gserviceaccount.com + +terraform -chdir=infra/github-actions-wif import \ + google_iam_workload_identity_pool.github_actions \ + projects/swift2023groupc/locations/global/workloadIdentityPools/dotto + +terraform -chdir=infra/github-actions-wif import \ + google_iam_workload_identity_pool_provider.github \ + projects/swift2023groupc/locations/global/workloadIdentityPools/dotto/providers/github +``` + +import 後に plan を確認し、既存設定との差分が意図したものだけになっていることを確認してから apply します。 + +```bash +mise exec terraform -- terraform -chdir=infra/github-actions-wif plan +mise exec terraform -- terraform -chdir=infra/github-actions-wif apply +``` + +`flutterfire configure` が Firebase app を新規作成する必要がある場合は、`terraform.tfvars` の `service_account_project_roles` を `["roles/firebase.admin"]` に変更してください。 + +## GitHub Actions variables + +`terraform apply` 後の output を GitHub Actions variables に設定します。 + +```bash +gh variable set GCP_SERVICE_ACCOUNT --body "$(terraform -chdir=infra/github-actions-wif output -raw service_account_email)" +gh variable set GCP_WORKLOAD_IDENTITY_PROVIDER --body "$(terraform -chdir=infra/github-actions-wif output -raw workload_identity_provider)" +``` + +workflow では次のように使います。 + +```yaml +permissions: + contents: read + id-token: write + +steps: + - uses: google-github-actions/auth@v3 + with: + project_id: swift2023groupc + workload_identity_provider: ${{ vars.GCP_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ vars.GCP_SERVICE_ACCOUNT }} +``` diff --git a/infra/github-actions-wif/main.tf b/infra/github-actions-wif/main.tf new file mode 100644 index 00000000..10f28371 --- /dev/null +++ b/infra/github-actions-wif/main.tf @@ -0,0 +1,70 @@ +locals { + github_repository_owner = split("/", var.github_repository)[0] + ref_condition = length(var.allowed_refs) > 0 ? "assertion.ref in ${jsonencode(var.allowed_refs)}" : "" + attribute_condition = join(" && ", compact([ + "assertion.repository == ${jsonencode(var.github_repository)}", + "assertion.repository_owner == ${jsonencode(local.github_repository_owner)}", + local.ref_condition, + ])) +} + +resource "google_project_service" "required" { + for_each = var.enable_required_apis ? var.required_services : toset([]) + + project = var.project_id + service = each.value + disable_on_destroy = false +} + +resource "google_service_account" "github_actions" { + project = var.project_id + account_id = var.service_account_id + display_name = var.service_account_display_name + + depends_on = [google_project_service.required] +} + +resource "google_project_iam_member" "github_actions_project_roles" { + for_each = var.service_account_project_roles + + project = var.project_id + role = each.value + member = google_service_account.github_actions.member +} + +resource "google_iam_workload_identity_pool" "github_actions" { + project = var.project_id + workload_identity_pool_id = var.workload_identity_pool_id + display_name = "GitHub Actions" + description = "OIDC pool for GitHub Actions." + + depends_on = [google_project_service.required] +} + +resource "google_iam_workload_identity_pool_provider" "github" { + project = var.project_id + workload_identity_pool_id = google_iam_workload_identity_pool.github_actions.workload_identity_pool_id + workload_identity_pool_provider_id = var.workload_identity_pool_provider_id + display_name = "GitHub" + description = "Trusts GitHub Actions OIDC tokens for ${var.github_repository}." + attribute_condition = local.attribute_condition + + attribute_mapping = { + "google.subject" = "assertion.sub" + "attribute.actor" = "assertion.actor" + "attribute.repository" = "assertion.repository" + "attribute.repository_owner" = "assertion.repository_owner" + "attribute.ref" = "assertion.ref" + "attribute.workflow" = "assertion.workflow" + } + + oidc { + issuer_uri = "https://token.actions.githubusercontent.com" + } +} + +resource "google_service_account_iam_member" "github_actions_workload_identity_user" { + service_account_id = google_service_account.github_actions.name + role = "roles/iam.workloadIdentityUser" + member = "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.github_actions.name}/attribute.repository/${var.github_repository}" +} diff --git a/infra/github-actions-wif/outputs.tf b/infra/github-actions-wif/outputs.tf new file mode 100644 index 00000000..0061e5bc --- /dev/null +++ b/infra/github-actions-wif/outputs.tf @@ -0,0 +1,17 @@ +output "service_account_email" { + description = "Service account email to pass to google-github-actions/auth." + value = google_service_account.github_actions.email +} + +output "workload_identity_provider" { + description = "Workload Identity Provider resource name to pass to google-github-actions/auth." + value = google_iam_workload_identity_pool_provider.github.name +} + +output "github_actions_variables" { + description = "Values to register as GitHub Actions variables." + value = { + GCP_SERVICE_ACCOUNT = google_service_account.github_actions.email + GCP_WORKLOAD_IDENTITY_PROVIDER = google_iam_workload_identity_pool_provider.github.name + } +} diff --git a/infra/github-actions-wif/terraform.tfvars.example b/infra/github-actions-wif/terraform.tfvars.example new file mode 100644 index 00000000..f43e1f48 --- /dev/null +++ b/infra/github-actions-wif/terraform.tfvars.example @@ -0,0 +1,16 @@ +project_id = "swift2023groupc" + +github_repository = "fun-dotto/app" + +service_account_id = "github-actions" + +workload_identity_pool_id = "dotto" + +workload_identity_pool_provider_id = "github" + +# Keep this empty because CI runs on both pushes and pull requests. +# Set refs such as ["refs/heads/main"] only when the workflow should authenticate from specific refs. +allowed_refs = [] + +# Use roles/firebase.admin instead if CI must create Firebase apps during flutterfire configure. +service_account_project_roles = ["roles/firebase.viewer"] diff --git a/infra/github-actions-wif/variables.tf b/infra/github-actions-wif/variables.tf new file mode 100644 index 00000000..ba9e7c6d --- /dev/null +++ b/infra/github-actions-wif/variables.tf @@ -0,0 +1,75 @@ +variable "project_id" { + description = "Google Cloud project ID that owns the Firebase project." + type = string +} + +variable "github_repository" { + description = "GitHub repository allowed to impersonate the service account, formatted as owner/repo." + type = string + default = "fun-dotto/app" + + validation { + condition = can(regex("^[^/]+/[^/]+$", var.github_repository)) + error_message = "github_repository must be formatted as owner/repo." + } +} + +variable "allowed_refs" { + description = "Git refs allowed to authenticate through this provider. Set to [] to allow every ref in github_repository." + type = list(string) + default = [] +} + +variable "service_account_id" { + description = "Service account ID for GitHub Actions. Must be unique in the project and at most 30 characters." + type = string + default = "github-actions" + + validation { + condition = can(regex("^[a-z][a-z0-9-]{4,28}[a-z0-9]$", var.service_account_id)) + error_message = "service_account_id must be 6-30 characters, start with a lowercase letter, and contain only lowercase letters, numbers, and hyphens." + } +} + +variable "service_account_display_name" { + description = "Display name for the GitHub Actions service account." + type = string + default = "GitHub Actions" +} + +variable "workload_identity_pool_id" { + description = "Workload Identity Pool ID for GitHub Actions." + type = string + default = "dotto" +} + +variable "workload_identity_pool_provider_id" { + description = "Workload Identity Pool Provider ID for GitHub OIDC." + type = string + default = "github" +} + +variable "service_account_project_roles" { + description = "Project roles granted to the GitHub Actions service account." + type = set(string) + default = ["roles/firebase.viewer"] +} + +variable "enable_required_apis" { + description = "Whether Terraform should enable APIs needed for Workload Identity Federation and Firebase lookups." + type = bool + default = true +} + +variable "required_services" { + description = "Google Cloud APIs required by this Terraform module." + type = set(string) + default = [ + "cloudresourcemanager.googleapis.com", + "firebase.googleapis.com", + "iam.googleapis.com", + "iamcredentials.googleapis.com", + "serviceusage.googleapis.com", + "sts.googleapis.com", + ] +} diff --git a/infra/github-actions-wif/versions.tf b/infra/github-actions-wif/versions.tf new file mode 100644 index 00000000..a6fcaf3f --- /dev/null +++ b/infra/github-actions-wif/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.6.0" + + required_providers { + google = { + source = "hashicorp/google" + version = "~> 7.0" + } + } +} + +provider "google" { + project = var.project_id +} diff --git a/mise.toml b/mise.toml index 580f1270..3e388db0 100644 --- a/mise.toml +++ b/mise.toml @@ -16,6 +16,7 @@ node = "24.15.0" "npm:firebase-tools" = "15.15.0" "npm:@openapitools/openapi-generator-cli" = "2.32.0" ruby = "4.0.4" +terraform = "1.15.5" [tasks.pre-commit] run = "dart run scripts/pre_commit.dart" @@ -27,14 +28,16 @@ run = [ "dart pub global activate flutterfire_cli 1.3.2", "melos bootstrap", "melos run setup:flutterfire", - "melos run fetch:dart_defines" + "melos run fetch:dart_defines", ] [tasks.setup-ci] run = [ "bundle install", "dart pub global activate melos 7.8.0", - "melos bootstrap" + "dart pub global activate flutterfire_cli 1.3.2", + "melos bootstrap", + "melos run setup:flutterfire", ] [tasks.build] @@ -63,3 +66,9 @@ run = "dart fix --apply && dart format apps/dotto/ packages/dotto_design_system/ [tasks.generate-openapi] run = "openapi-generator-cli generate -i ./openapi/openapi.yaml -g dart-dio -o ./packages/dotto_api" + +[tasks.terraform-fmt] +run = "terraform -chdir=infra/github-actions-wif fmt" + +[tasks.terraform-validate] +run = "terraform -chdir=infra/github-actions-wif validate" diff --git a/pubspec.yaml b/pubspec.yaml index 82410d7f..d4f15947 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -45,7 +45,7 @@ melos: packageFilters: scope: dotto setup:flutterfire: - run: flutterfire configure --platforms="ios,android" --yes + run: flutterfire configure --project=swift2023groupc --platforms="ios,android" --yes exec: concurrency: 1 packageFilters: