Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ concurrency:

permissions:
contents: read
id-token: write

defaults:
run:
Expand All @@ -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

Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
22 changes: 22 additions & 0 deletions infra/github-actions-wif/.terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

67 changes: 67 additions & 0 deletions infra/github-actions-wif/README.md
Original file line number Diff line number Diff line change
@@ -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 }}
```
70 changes: 70 additions & 0 deletions infra/github-actions-wif/main.tf
Original file line number Diff line number Diff line change
@@ -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}"
}
17 changes: 17 additions & 0 deletions infra/github-actions-wif/outputs.tf
Original file line number Diff line number Diff line change
@@ -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
}
}
16 changes: 16 additions & 0 deletions infra/github-actions-wif/terraform.tfvars.example
Original file line number Diff line number Diff line change
@@ -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"]
75 changes: 75 additions & 0 deletions infra/github-actions-wif/variables.tf
Original file line number Diff line number Diff line change
@@ -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",
]
}
14 changes: 14 additions & 0 deletions infra/github-actions-wif/versions.tf
Original file line number Diff line number Diff line change
@@ -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
}
13 changes: 11 additions & 2 deletions mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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]
Expand Down Expand Up @@ -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"
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down