diff --git a/.github/workflows/auto-label--deploy-trigger.yaml b/.github/workflows/auto-label--deploy-trigger.yaml index a8fa2938..781dff92 100644 --- a/.github/workflows/auto-label--deploy-trigger.yaml +++ b/.github/workflows/auto-label--deploy-trigger.yaml @@ -26,6 +26,12 @@ jobs: targets: ${{ steps.resolver.outputs.targets }} has-targets: ${{ steps.resolver.outputs.has-targets }} steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Validate apply_trigger + run: bash scripts/terragrunt-release/validate-apply-trigger.sh + - name: Generate GitHub App token id: app-token uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0 @@ -98,12 +104,15 @@ jobs: strategy: matrix: target: ${{ fromJson(needs.deploy-trigger.outputs.targets) }} + exclude: + - target: + stack: terragrunt-shared fail-fast: false uses: ./.github/workflows/reusable--terragrunt-executor.yaml with: service-name: ${{ matrix.target.service }} environment: ${{ matrix.target.environment }} - action-type: ${{ matrix.target.stack == 'terragrunt' && (github.event_name == 'pull_request' && 'plan' || 'apply') || '' }} + action-type: ${{ matrix.target.stack == 'terragrunt' && (github.event_name == 'pull_request' && 'plan' || (matrix.target.apply_trigger == 'merge' && 'apply' || '')) || '' }} iam-role: ${{ github.event_name == 'pull_request' && matrix.target.iam_role_plan || matrix.target.iam_role_apply }} aws-region: ${{ matrix.target.aws_region }} working-directory: ${{ matrix.target.working_directory }} @@ -123,6 +132,8 @@ jobs: exclude: - target: stack: terragrunt + - target: + stack: terragrunt-shared fail-fast: false uses: ./.github/workflows/reusable--kubernetes-builder.yaml with: diff --git a/.github/workflows/auto-label--label-dispatcher.yaml b/.github/workflows/auto-label--label-dispatcher.yaml index a505c5b1..b5073c6c 100644 --- a/.github/workflows/auto-label--label-dispatcher.yaml +++ b/.github/workflows/auto-label--label-dispatcher.yaml @@ -14,6 +14,7 @@ permissions: jobs: dispatch-labels: name: 'Dispatch Labels' + if: ${{ !startsWith(github.head_ref, 'release-please--') }} runs-on: ubuntu-latest steps: - name: Generate GitHub App token diff --git a/.github/workflows/auto-release--plan.yaml b/.github/workflows/auto-release--plan.yaml new file mode 100644 index 00000000..40051095 --- /dev/null +++ b/.github/workflows/auto-release--plan.yaml @@ -0,0 +1,57 @@ +name: 'Auto Release - Plan' + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + id-token: write + contents: read + pull-requests: write + +concurrency: + group: auto-release-plan-${{ github.head_ref }} + cancel-in-progress: true + +jobs: + resolve: + name: 'Resolve Release Targets' + if: startsWith(github.head_ref, 'release-please--branches--main--components--') + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + targets: ${{ steps.resolve.outputs.targets }} + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Resolve release targets + id: resolve + env: + HEAD_REF: ${{ github.head_ref }} + run: | + set -euo pipefail + service=$(bash scripts/terragrunt-release/release-service.sh --branch "$HEAD_REF") + targets=$(bash scripts/terragrunt-release/release-targets.sh "$service") + echo "targets=$targets" >> "$GITHUB_OUTPUT" + + plan: + name: 'Plan Release (${{ matrix.target.service }}:${{ matrix.target.environment }})' + needs: resolve + if: needs.resolve.outputs.targets != '[]' + strategy: + matrix: + target: ${{ fromJson(needs.resolve.outputs.targets) }} + fail-fast: false + uses: ./.github/workflows/reusable--terragrunt-executor.yaml + with: + service-name: ${{ matrix.target.service }} + environment: ${{ matrix.target.environment }} + action-type: plan + iam-role: ${{ matrix.target.iam_role_plan }} + aws-region: ${{ matrix.target.aws_region }} + working-directory: ${{ matrix.target.working_directory }} + app-id: ${{ vars.APP_ID }} + secrets: + private-key: ${{ secrets.APP_PRIVATE_KEY }} diff --git a/.github/workflows/auto-release--trigger.yaml b/.github/workflows/auto-release--trigger.yaml new file mode 100644 index 00000000..62222d7d --- /dev/null +++ b/.github/workflows/auto-release--trigger.yaml @@ -0,0 +1,71 @@ +name: 'Auto Release - Trigger' + +on: + release: + types: [published] + workflow_dispatch: + inputs: + tag: + description: 'Release tag to (re-)apply (e.g. vpc-v0.2.0)' + required: true + type: string + +permissions: + id-token: write + contents: read + pull-requests: write + +concurrency: + group: auto-release-trigger-${{ github.event.release.tag_name || inputs.tag }} + cancel-in-progress: false + +jobs: + resolve: + name: 'Resolve Release Targets' + runs-on: ubuntu-latest + permissions: + contents: read + env: + TAG: ${{ github.event.release.tag_name || inputs.tag }} + outputs: + targets: ${{ steps.resolve.outputs.targets }} + tag: ${{ steps.resolve.outputs.tag }} + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ env.TAG }} + + - name: Resolve release targets + id: resolve + run: | + set -euo pipefail + service=$(bash scripts/terragrunt-release/release-service.sh --tag "$TAG") + targets=$(bash scripts/terragrunt-release/release-targets.sh "$service") + echo "targets=$targets" >> "$GITHUB_OUTPUT" + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + + apply: + name: 'Apply Release (${{ matrix.target.service }}:${{ matrix.target.environment }})' + needs: resolve + if: needs.resolve.outputs.targets != '[]' + concurrency: + group: terragrunt-apply-${{ matrix.target.working_directory }} + cancel-in-progress: false + strategy: + matrix: + target: ${{ fromJson(needs.resolve.outputs.targets) }} + fail-fast: false + uses: ./.github/workflows/reusable--terragrunt-executor.yaml + with: + service-name: ${{ matrix.target.service }} + environment: ${{ matrix.target.environment }} + action-type: apply + iam-role: ${{ matrix.target.iam_role_apply }} + aws-region: ${{ matrix.target.aws_region }} + working-directory: ${{ matrix.target.working_directory }} + app-id: ${{ vars.APP_ID }} + ref: ${{ needs.resolve.outputs.tag }} + github-environment: ${{ matrix.target.environment }} + secrets: + private-key: ${{ secrets.APP_PRIVATE_KEY }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e44e6ea7..05beb1ea 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,13 +3,38 @@ name: Release on: push: branches: [main] + workflow_dispatch: {} permissions: contents: read +concurrency: + group: release-please + cancel-in-progress: false + jobs: + discover: + runs-on: ubuntu-latest + outputs: + paths: ${{ steps.find.outputs.paths }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Find release-please configs + id: find + run: | + set -euo pipefail + paths=$(git ls-files '*/release-please-config.json' | jq -R 'rtrimstr("/release-please-config.json")' | jq -sc .) + echo "paths=$paths" >> "$GITHUB_OUTPUT" + release-please: + needs: discover + if: needs.discover.outputs.paths != '[]' runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + path: ${{ fromJson(needs.discover.outputs.paths) }} steps: - name: Generate GitHub App token id: app-token @@ -19,7 +44,7 @@ jobs: private-key: ${{ secrets.APP_PRIVATE_KEY }} - uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5 - id: release with: token: ${{ steps.app-token.outputs.token }} - release-type: simple + config-file: ${{ matrix.path }}/release-please-config.json + manifest-file: ${{ matrix.path }}/release-please-manifest.json diff --git a/.github/workflows/reusable--terragrunt-executor.yaml b/.github/workflows/reusable--terragrunt-executor.yaml index d44f6e15..24d7d7be 100644 --- a/.github/workflows/reusable--terragrunt-executor.yaml +++ b/.github/workflows/reusable--terragrunt-executor.yaml @@ -31,6 +31,16 @@ on: required: true type: string description: 'GitHub App ID for authentication' + ref: + required: false + type: string + default: '' + description: 'Git ref to check out (defaults to the triggering ref)' + github-environment: + required: false + type: string + default: '' + description: 'GitHub environment the job runs in (none when empty)' secrets: private-key: required: true @@ -41,6 +51,7 @@ jobs: name: 'Execute Terragrunt' if: inputs.action-type != '' runs-on: ubuntu-latest + environment: ${{ inputs.github-environment }} steps: - name: Generate GitHub App token id: app-token @@ -52,6 +63,8 @@ jobs: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ inputs.ref }} - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@e1253824e5c10ff9df46874f81ed3ec929e19cfd # v6.3.0 diff --git a/.github/workflows/test-terragrunt-release.yaml b/.github/workflows/test-terragrunt-release.yaml new file mode 100644 index 00000000..6b15a6fc --- /dev/null +++ b/.github/workflows/test-terragrunt-release.yaml @@ -0,0 +1,19 @@ +name: Test Terragrunt Release + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + test: + name: Test terragrunt release scripts + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - run: bash scripts/terragrunt-release/test.sh + - run: bash scripts/terragrunt-release/validate-apply-trigger.sh + - run: bash scripts/terragrunt-release/check-release-configs.sh diff --git a/README-ja.md b/README-ja.md index 6257adf7..1fcc9b41 100644 --- a/README-ja.md +++ b/README-ja.md @@ -35,6 +35,17 @@ - `auto-label--deploy-trigger.yaml` が PR ラベル / `main` への push で起動する。 - `panicboat/deploy-actions/label-resolver` が `workflow-config.yaml` を参照して各ラベルを 1 つ以上の `{service}/{environment}` ターゲットに展開する。 +- `apply_trigger: merge` の Terragrunt environment は `main` への push で apply される。 +- `apply_trigger: release` の Terragrunt environment は `aws/{service}` の release 公開時に `auto-release--trigger.yaml` が apply する。 + - release PR では `auto-release--plan.yaml` が `terragrunt plan` を実行する。plan を出し直すときは該当する `auto-release--plan` の run で「Re-run all jobs」を使う。 + - release の apply を再実行するときは、`auto-release--trigger` を release tag の ref で `workflow_dispatch` 起動し、同じ tag を渡す:`gh workflow run auto-release--trigger.yaml --ref {service}-vX.Y.Z -f tag={service}-vX.Y.Z`。 +- `workflow-config.yaml` の `terragrunt` stack の `exclude` の rule に一致する `{service}/{environment}` の組は、`label-resolver` の target(PR の plan・`main` への push の apply)にならず、release の plan / apply も行われない。 + - rule のキー(`service`、`environment`)がすべて組の値と一致したとき、その組に一致する。`environment` だけの rule はすべての service に効く。 + - release PR は作られるが、merge しても apply されない:`release-targets.sh` が target を返さないとき、`auto-release--plan.yaml` と `auto-release--trigger.yaml` は `plan` / `apply` を skip する。 +- `aws/{service}/` 配下の environment ディレクトリ以外のファイル(module、`root.hcl` など)の変更は `terragrunt-shared` stack で検出され、その service の全 environment が対象になる。 +- `apply_trigger: release` の environment ディレクトリを持つ service には `aws/{service}/release-please-config.json` と `release-please-manifest.json` が必要。無い場合は `check-release-configs.sh` が CI を失敗させる。 + - `apply_trigger: merge` の environment ディレクトリだけを変える commit は、`release-please-config.json` の `exclude-paths` で release から外れる。 + - `check-release-configs.sh` は `exclude-paths` を `workflow-config.yaml` の `apply_trigger: merge` の environment と照合する。 ### Stacks @@ -47,6 +58,7 @@ ### Environments and Authentication - Environment、AWS region、IAM role は `workflow-config.yaml` で定義する。環境の追加・削除は同ファイルの編集で行う。 +- `workflow-config.yaml` の `apply_trigger` が environment を apply するタイミングを決める。`merge` は `main` への push、`release` は service の release 公開時に apply する。 - 認証は GitHub OIDC。`aws/github-oidc-auth/` が environment ごとの plan / apply role を発行し、executor がそれを引き受ける。 - Terragrunt の remote state は S3 + DynamoDB に保存される。bucket / lock table 名は各 `aws/{service}/root.hcl`(および `github/` 配下の同等ファイル)で定義される。 @@ -57,6 +69,7 @@ flowchart LR subgraph Triggers PRevent[PR open/sync] Mainpush[push main] + Release[release published] end subgraph Labeling @@ -75,6 +88,7 @@ flowchart LR end subgraph Terragrunt + ReleaseTrigger[auto-release--trigger] TG[reusable--terragrunt-executor] Plan[terragrunt plan] Apply[terragrunt apply] @@ -91,8 +105,10 @@ flowchart LR Dispatcher --> Trigger Trigger --> Resolver Resolver -->|stack: terragrunt| TG + Release --> ReleaseTrigger + ReleaseTrigger --> TG TG -->|on pull_request| Plan - TG -->|on push main| Apply + TG -->|on push main or release published| Apply Plan --> PRComment Apply --> AWS Resolver -->|stack: kubernetes| Group diff --git a/README.md b/README.md index a7d2ee81..e255a772 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,17 @@ A PR labeled by `panicboat/deploy-actions` is resolved against `workflow-config. - `auto-label--deploy-trigger.yaml` runs on PR labels and on push to `main`. - `panicboat/deploy-actions/label-resolver` reads `workflow-config.yaml` to expand each label into one or more `{service}/{environment}` targets. +- Terragrunt environments with `apply_trigger: merge` are applied on push to `main`. +- Terragrunt environments with `apply_trigger: release` are applied by `auto-release--trigger.yaml` when the `aws/{service}` release is published. + - The release PR runs `terragrunt plan` through `auto-release--plan.yaml`; to re-plan, use "Re-run all jobs" on that `auto-release--plan` run. + - To re-apply a release, run `auto-release--trigger` with `workflow_dispatch` on the release tag ref and pass the same tag: `gh workflow run auto-release--trigger.yaml --ref {service}-vX.Y.Z -f tag={service}-vX.Y.Z`. +- A `{service}/{environment}` pair matched by a rule in `exclude` of the `terragrunt` stack in `workflow-config.yaml` gets no target from `label-resolver` (PR plan, apply on push to `main`) and no release plan / apply. + - A rule matches a pair when every key (`service`, `environment`) equals the pair's value; a rule with only `environment` covers every service. + - The release PR is still created, but merging it applies nothing: `auto-release--plan.yaml` and `auto-release--trigger.yaml` skip `plan` / `apply` when `release-targets.sh` returns no target. +- Changes to files outside environment directories under `aws/{service}/` (modules, `root.hcl`, ...) are detected through the `terragrunt-shared` stack and target every environment of that service. +- A service with an `apply_trigger: release` environment directory needs `aws/{service}/release-please-config.json` and `release-please-manifest.json`; `check-release-configs.sh` fails CI otherwise. + - Commits that change only `apply_trigger: merge` environment directories are kept out of the release through `exclude-paths` in `release-please-config.json`. + - `check-release-configs.sh` compares `exclude-paths` with the `apply_trigger: merge` environments in `workflow-config.yaml`. ### Stacks @@ -47,6 +58,7 @@ A PR labeled by `panicboat/deploy-actions` is resolved against `workflow-config. ### Environments and authentication - Environments, AWS regions, and IAM roles are defined in `workflow-config.yaml`. Adding or removing an environment means editing that file. +- `apply_trigger` in `workflow-config.yaml` decides when an environment is applied: `merge` applies on push to `main`, `release` applies when the service release is published. - Authentication uses GitHub OIDC. `aws/github-oidc-auth/` issues per-environment plan / apply roles that the executors assume. - Terragrunt remote state lives in S3 + DynamoDB; bucket and lock-table names are defined in each `aws/{service}/root.hcl` (and the equivalent under `github/`). @@ -57,6 +69,7 @@ flowchart LR subgraph Triggers PRevent[PR open/sync] Mainpush[push main] + Release[release published] end subgraph Labeling @@ -75,6 +88,7 @@ flowchart LR end subgraph Terragrunt + ReleaseTrigger[auto-release--trigger] TG[reusable--terragrunt-executor] Plan[terragrunt plan] Apply[terragrunt apply] @@ -91,8 +105,10 @@ flowchart LR Dispatcher --> Trigger Trigger --> Resolver Resolver -->|stack: terragrunt| TG + Release --> ReleaseTrigger + ReleaseTrigger --> TG TG -->|on pull_request| Plan - TG -->|on push main| Apply + TG -->|on push main or release published| Apply Plan --> PRComment Apply --> AWS Resolver -->|stack: kubernetes| Group diff --git a/aws/alb/release-please-config.json b/aws/alb/release-please-config.json new file mode 100644 index 00000000..e84de794 --- /dev/null +++ b/aws/alb/release-please-config.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "bootstrap-sha": "f56a231752904c9c78e96d955fc497a99fcd65b8", + "separate-pull-requests": true, + "label": "autorelease: pending,release: alb", + "changelog-sections": [ + { "type": "feat", "section": "Features" }, + { "type": "fix", "section": "Bug Fixes" }, + { "type": "perf", "section": "Performance Improvements" }, + { "type": "revert", "section": "Reverts" }, + { "type": "refactor", "section": "Code Refactoring" }, + { "type": "chore", "section": "Miscellaneous Chores" }, + { "type": "docs", "section": "Documentation", "hidden": true }, + { "type": "style", "section": "Styles", "hidden": true }, + { "type": "test", "section": "Tests", "hidden": true }, + { "type": "build", "section": "Build System", "hidden": true }, + { "type": "ci", "section": "Continuous Integration", "hidden": true } + ], + "packages": { + "aws/alb": { + "release-type": "simple", + "component": "alb", + "include-component-in-tag": true, + "exclude-paths": ["aws/alb/master", "aws/alb/develop"] + } + } +} diff --git a/aws/alb/release-please-manifest.json b/aws/alb/release-please-manifest.json new file mode 100644 index 00000000..7b7af862 --- /dev/null +++ b/aws/alb/release-please-manifest.json @@ -0,0 +1,3 @@ +{ + "aws/alb": "0.1.0" +} diff --git a/aws/eks-holmesgpt/release-please-config.json b/aws/eks-holmesgpt/release-please-config.json new file mode 100644 index 00000000..e4b167d6 --- /dev/null +++ b/aws/eks-holmesgpt/release-please-config.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "bootstrap-sha": "f56a231752904c9c78e96d955fc497a99fcd65b8", + "separate-pull-requests": true, + "label": "autorelease: pending,release: eks-holmesgpt", + "changelog-sections": [ + { "type": "feat", "section": "Features" }, + { "type": "fix", "section": "Bug Fixes" }, + { "type": "perf", "section": "Performance Improvements" }, + { "type": "revert", "section": "Reverts" }, + { "type": "refactor", "section": "Code Refactoring" }, + { "type": "chore", "section": "Miscellaneous Chores" }, + { "type": "docs", "section": "Documentation", "hidden": true }, + { "type": "style", "section": "Styles", "hidden": true }, + { "type": "test", "section": "Tests", "hidden": true }, + { "type": "build", "section": "Build System", "hidden": true }, + { "type": "ci", "section": "Continuous Integration", "hidden": true } + ], + "packages": { + "aws/eks-holmesgpt": { + "release-type": "simple", + "component": "eks-holmesgpt", + "include-component-in-tag": true, + "exclude-paths": ["aws/eks-holmesgpt/master", "aws/eks-holmesgpt/develop"] + } + } +} diff --git a/aws/eks-holmesgpt/release-please-manifest.json b/aws/eks-holmesgpt/release-please-manifest.json new file mode 100644 index 00000000..3f5db3ad --- /dev/null +++ b/aws/eks-holmesgpt/release-please-manifest.json @@ -0,0 +1,3 @@ +{ + "aws/eks-holmesgpt": "0.1.0" +} diff --git a/aws/eks-karpenter/release-please-config.json b/aws/eks-karpenter/release-please-config.json new file mode 100644 index 00000000..b530bb13 --- /dev/null +++ b/aws/eks-karpenter/release-please-config.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "bootstrap-sha": "f56a231752904c9c78e96d955fc497a99fcd65b8", + "separate-pull-requests": true, + "label": "autorelease: pending,release: eks-karpenter", + "changelog-sections": [ + { "type": "feat", "section": "Features" }, + { "type": "fix", "section": "Bug Fixes" }, + { "type": "perf", "section": "Performance Improvements" }, + { "type": "revert", "section": "Reverts" }, + { "type": "refactor", "section": "Code Refactoring" }, + { "type": "chore", "section": "Miscellaneous Chores" }, + { "type": "docs", "section": "Documentation", "hidden": true }, + { "type": "style", "section": "Styles", "hidden": true }, + { "type": "test", "section": "Tests", "hidden": true }, + { "type": "build", "section": "Build System", "hidden": true }, + { "type": "ci", "section": "Continuous Integration", "hidden": true } + ], + "packages": { + "aws/eks-karpenter": { + "release-type": "simple", + "component": "eks-karpenter", + "include-component-in-tag": true, + "exclude-paths": ["aws/eks-karpenter/master", "aws/eks-karpenter/develop"] + } + } +} diff --git a/aws/eks-karpenter/release-please-manifest.json b/aws/eks-karpenter/release-please-manifest.json new file mode 100644 index 00000000..ce3e45be --- /dev/null +++ b/aws/eks-karpenter/release-please-manifest.json @@ -0,0 +1,3 @@ +{ + "aws/eks-karpenter": "0.1.0" +} diff --git a/aws/eks-logs/release-please-config.json b/aws/eks-logs/release-please-config.json new file mode 100644 index 00000000..fc84d36d --- /dev/null +++ b/aws/eks-logs/release-please-config.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "bootstrap-sha": "f56a231752904c9c78e96d955fc497a99fcd65b8", + "separate-pull-requests": true, + "label": "autorelease: pending,release: eks-logs", + "changelog-sections": [ + { "type": "feat", "section": "Features" }, + { "type": "fix", "section": "Bug Fixes" }, + { "type": "perf", "section": "Performance Improvements" }, + { "type": "revert", "section": "Reverts" }, + { "type": "refactor", "section": "Code Refactoring" }, + { "type": "chore", "section": "Miscellaneous Chores" }, + { "type": "docs", "section": "Documentation", "hidden": true }, + { "type": "style", "section": "Styles", "hidden": true }, + { "type": "test", "section": "Tests", "hidden": true }, + { "type": "build", "section": "Build System", "hidden": true }, + { "type": "ci", "section": "Continuous Integration", "hidden": true } + ], + "packages": { + "aws/eks-logs": { + "release-type": "simple", + "component": "eks-logs", + "include-component-in-tag": true, + "exclude-paths": ["aws/eks-logs/master", "aws/eks-logs/develop"] + } + } +} diff --git a/aws/eks-logs/release-please-manifest.json b/aws/eks-logs/release-please-manifest.json new file mode 100644 index 00000000..650bb4ba --- /dev/null +++ b/aws/eks-logs/release-please-manifest.json @@ -0,0 +1,3 @@ +{ + "aws/eks-logs": "0.1.0" +} diff --git a/aws/eks-metrics/release-please-config.json b/aws/eks-metrics/release-please-config.json new file mode 100644 index 00000000..4119efd6 --- /dev/null +++ b/aws/eks-metrics/release-please-config.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "bootstrap-sha": "f56a231752904c9c78e96d955fc497a99fcd65b8", + "separate-pull-requests": true, + "label": "autorelease: pending,release: eks-metrics", + "changelog-sections": [ + { "type": "feat", "section": "Features" }, + { "type": "fix", "section": "Bug Fixes" }, + { "type": "perf", "section": "Performance Improvements" }, + { "type": "revert", "section": "Reverts" }, + { "type": "refactor", "section": "Code Refactoring" }, + { "type": "chore", "section": "Miscellaneous Chores" }, + { "type": "docs", "section": "Documentation", "hidden": true }, + { "type": "style", "section": "Styles", "hidden": true }, + { "type": "test", "section": "Tests", "hidden": true }, + { "type": "build", "section": "Build System", "hidden": true }, + { "type": "ci", "section": "Continuous Integration", "hidden": true } + ], + "packages": { + "aws/eks-metrics": { + "release-type": "simple", + "component": "eks-metrics", + "include-component-in-tag": true, + "exclude-paths": ["aws/eks-metrics/master", "aws/eks-metrics/develop"] + } + } +} diff --git a/aws/eks-metrics/release-please-manifest.json b/aws/eks-metrics/release-please-manifest.json new file mode 100644 index 00000000..7ebf5639 --- /dev/null +++ b/aws/eks-metrics/release-please-manifest.json @@ -0,0 +1,3 @@ +{ + "aws/eks-metrics": "0.1.0" +} diff --git a/aws/eks-secrets/release-please-config.json b/aws/eks-secrets/release-please-config.json new file mode 100644 index 00000000..b8efd5aa --- /dev/null +++ b/aws/eks-secrets/release-please-config.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "bootstrap-sha": "f56a231752904c9c78e96d955fc497a99fcd65b8", + "separate-pull-requests": true, + "label": "autorelease: pending,release: eks-secrets", + "changelog-sections": [ + { "type": "feat", "section": "Features" }, + { "type": "fix", "section": "Bug Fixes" }, + { "type": "perf", "section": "Performance Improvements" }, + { "type": "revert", "section": "Reverts" }, + { "type": "refactor", "section": "Code Refactoring" }, + { "type": "chore", "section": "Miscellaneous Chores" }, + { "type": "docs", "section": "Documentation", "hidden": true }, + { "type": "style", "section": "Styles", "hidden": true }, + { "type": "test", "section": "Tests", "hidden": true }, + { "type": "build", "section": "Build System", "hidden": true }, + { "type": "ci", "section": "Continuous Integration", "hidden": true } + ], + "packages": { + "aws/eks-secrets": { + "release-type": "simple", + "component": "eks-secrets", + "include-component-in-tag": true, + "exclude-paths": ["aws/eks-secrets/master", "aws/eks-secrets/develop"] + } + } +} diff --git a/aws/eks-secrets/release-please-manifest.json b/aws/eks-secrets/release-please-manifest.json new file mode 100644 index 00000000..c1f57fcd --- /dev/null +++ b/aws/eks-secrets/release-please-manifest.json @@ -0,0 +1,3 @@ +{ + "aws/eks-secrets": "0.1.0" +} diff --git a/aws/eks-traces/release-please-config.json b/aws/eks-traces/release-please-config.json new file mode 100644 index 00000000..1ab25d0a --- /dev/null +++ b/aws/eks-traces/release-please-config.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "bootstrap-sha": "f56a231752904c9c78e96d955fc497a99fcd65b8", + "separate-pull-requests": true, + "label": "autorelease: pending,release: eks-traces", + "changelog-sections": [ + { "type": "feat", "section": "Features" }, + { "type": "fix", "section": "Bug Fixes" }, + { "type": "perf", "section": "Performance Improvements" }, + { "type": "revert", "section": "Reverts" }, + { "type": "refactor", "section": "Code Refactoring" }, + { "type": "chore", "section": "Miscellaneous Chores" }, + { "type": "docs", "section": "Documentation", "hidden": true }, + { "type": "style", "section": "Styles", "hidden": true }, + { "type": "test", "section": "Tests", "hidden": true }, + { "type": "build", "section": "Build System", "hidden": true }, + { "type": "ci", "section": "Continuous Integration", "hidden": true } + ], + "packages": { + "aws/eks-traces": { + "release-type": "simple", + "component": "eks-traces", + "include-component-in-tag": true, + "exclude-paths": ["aws/eks-traces/master", "aws/eks-traces/develop"] + } + } +} diff --git a/aws/eks-traces/release-please-manifest.json b/aws/eks-traces/release-please-manifest.json new file mode 100644 index 00000000..24b94a17 --- /dev/null +++ b/aws/eks-traces/release-please-manifest.json @@ -0,0 +1,3 @@ +{ + "aws/eks-traces": "0.1.0" +} diff --git a/aws/eks/release-please-config.json b/aws/eks/release-please-config.json new file mode 100644 index 00000000..40d9e694 --- /dev/null +++ b/aws/eks/release-please-config.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "bootstrap-sha": "f56a231752904c9c78e96d955fc497a99fcd65b8", + "separate-pull-requests": true, + "label": "autorelease: pending,release: eks", + "changelog-sections": [ + { "type": "feat", "section": "Features" }, + { "type": "fix", "section": "Bug Fixes" }, + { "type": "perf", "section": "Performance Improvements" }, + { "type": "revert", "section": "Reverts" }, + { "type": "refactor", "section": "Code Refactoring" }, + { "type": "chore", "section": "Miscellaneous Chores" }, + { "type": "docs", "section": "Documentation", "hidden": true }, + { "type": "style", "section": "Styles", "hidden": true }, + { "type": "test", "section": "Tests", "hidden": true }, + { "type": "build", "section": "Build System", "hidden": true }, + { "type": "ci", "section": "Continuous Integration", "hidden": true } + ], + "packages": { + "aws/eks": { + "release-type": "simple", + "component": "eks", + "include-component-in-tag": true, + "exclude-paths": ["aws/eks/master", "aws/eks/develop"] + } + } +} diff --git a/aws/eks/release-please-manifest.json b/aws/eks/release-please-manifest.json new file mode 100644 index 00000000..1419c9de --- /dev/null +++ b/aws/eks/release-please-manifest.json @@ -0,0 +1,3 @@ +{ + "aws/eks": "0.1.0" +} diff --git a/aws/github-oidc-auth/release-please-config.json b/aws/github-oidc-auth/release-please-config.json new file mode 100644 index 00000000..1a553919 --- /dev/null +++ b/aws/github-oidc-auth/release-please-config.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "bootstrap-sha": "f56a231752904c9c78e96d955fc497a99fcd65b8", + "separate-pull-requests": true, + "label": "autorelease: pending,release: github-oidc-auth", + "changelog-sections": [ + { "type": "feat", "section": "Features" }, + { "type": "fix", "section": "Bug Fixes" }, + { "type": "perf", "section": "Performance Improvements" }, + { "type": "revert", "section": "Reverts" }, + { "type": "refactor", "section": "Code Refactoring" }, + { "type": "chore", "section": "Miscellaneous Chores" }, + { "type": "docs", "section": "Documentation", "hidden": true }, + { "type": "style", "section": "Styles", "hidden": true }, + { "type": "test", "section": "Tests", "hidden": true }, + { "type": "build", "section": "Build System", "hidden": true }, + { "type": "ci", "section": "Continuous Integration", "hidden": true } + ], + "packages": { + "aws/github-oidc-auth": { + "release-type": "simple", + "component": "github-oidc-auth", + "include-component-in-tag": true, + "exclude-paths": ["aws/github-oidc-auth/master", "aws/github-oidc-auth/develop"] + } + } +} diff --git a/aws/github-oidc-auth/release-please-manifest.json b/aws/github-oidc-auth/release-please-manifest.json new file mode 100644 index 00000000..014addd3 --- /dev/null +++ b/aws/github-oidc-auth/release-please-manifest.json @@ -0,0 +1,3 @@ +{ + "aws/github-oidc-auth": "0.1.0" +} diff --git a/aws/iam-service-linked-roles/release-please-config.json b/aws/iam-service-linked-roles/release-please-config.json new file mode 100644 index 00000000..74323ad0 --- /dev/null +++ b/aws/iam-service-linked-roles/release-please-config.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "bootstrap-sha": "f56a231752904c9c78e96d955fc497a99fcd65b8", + "separate-pull-requests": true, + "label": "autorelease: pending,release: iam-service-linked-roles", + "changelog-sections": [ + { "type": "feat", "section": "Features" }, + { "type": "fix", "section": "Bug Fixes" }, + { "type": "perf", "section": "Performance Improvements" }, + { "type": "revert", "section": "Reverts" }, + { "type": "refactor", "section": "Code Refactoring" }, + { "type": "chore", "section": "Miscellaneous Chores" }, + { "type": "docs", "section": "Documentation", "hidden": true }, + { "type": "style", "section": "Styles", "hidden": true }, + { "type": "test", "section": "Tests", "hidden": true }, + { "type": "build", "section": "Build System", "hidden": true }, + { "type": "ci", "section": "Continuous Integration", "hidden": true } + ], + "packages": { + "aws/iam-service-linked-roles": { + "release-type": "simple", + "component": "iam-service-linked-roles", + "include-component-in-tag": true, + "exclude-paths": ["aws/iam-service-linked-roles/master", "aws/iam-service-linked-roles/develop"] + } + } +} diff --git a/aws/iam-service-linked-roles/release-please-manifest.json b/aws/iam-service-linked-roles/release-please-manifest.json new file mode 100644 index 00000000..eaec75a6 --- /dev/null +++ b/aws/iam-service-linked-roles/release-please-manifest.json @@ -0,0 +1,3 @@ +{ + "aws/iam-service-linked-roles": "0.1.0" +} diff --git a/aws/secrets-manager/release-please-config.json b/aws/secrets-manager/release-please-config.json new file mode 100644 index 00000000..bae694a3 --- /dev/null +++ b/aws/secrets-manager/release-please-config.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "bootstrap-sha": "f56a231752904c9c78e96d955fc497a99fcd65b8", + "separate-pull-requests": true, + "label": "autorelease: pending,release: secrets-manager", + "changelog-sections": [ + { "type": "feat", "section": "Features" }, + { "type": "fix", "section": "Bug Fixes" }, + { "type": "perf", "section": "Performance Improvements" }, + { "type": "revert", "section": "Reverts" }, + { "type": "refactor", "section": "Code Refactoring" }, + { "type": "chore", "section": "Miscellaneous Chores" }, + { "type": "docs", "section": "Documentation", "hidden": true }, + { "type": "style", "section": "Styles", "hidden": true }, + { "type": "test", "section": "Tests", "hidden": true }, + { "type": "build", "section": "Build System", "hidden": true }, + { "type": "ci", "section": "Continuous Integration", "hidden": true } + ], + "packages": { + "aws/secrets-manager": { + "release-type": "simple", + "component": "secrets-manager", + "include-component-in-tag": true, + "exclude-paths": ["aws/secrets-manager/master", "aws/secrets-manager/develop"] + } + } +} diff --git a/aws/secrets-manager/release-please-manifest.json b/aws/secrets-manager/release-please-manifest.json new file mode 100644 index 00000000..c3401bc1 --- /dev/null +++ b/aws/secrets-manager/release-please-manifest.json @@ -0,0 +1,3 @@ +{ + "aws/secrets-manager": "0.1.0" +} diff --git a/aws/vpc/release-please-config.json b/aws/vpc/release-please-config.json new file mode 100644 index 00000000..2c44c983 --- /dev/null +++ b/aws/vpc/release-please-config.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "bootstrap-sha": "f56a231752904c9c78e96d955fc497a99fcd65b8", + "separate-pull-requests": true, + "label": "autorelease: pending,release: vpc", + "changelog-sections": [ + { "type": "feat", "section": "Features" }, + { "type": "fix", "section": "Bug Fixes" }, + { "type": "perf", "section": "Performance Improvements" }, + { "type": "revert", "section": "Reverts" }, + { "type": "refactor", "section": "Code Refactoring" }, + { "type": "chore", "section": "Miscellaneous Chores" }, + { "type": "docs", "section": "Documentation", "hidden": true }, + { "type": "style", "section": "Styles", "hidden": true }, + { "type": "test", "section": "Tests", "hidden": true }, + { "type": "build", "section": "Build System", "hidden": true }, + { "type": "ci", "section": "Continuous Integration", "hidden": true } + ], + "packages": { + "aws/vpc": { + "release-type": "simple", + "component": "vpc", + "include-component-in-tag": true, + "exclude-paths": ["aws/vpc/master", "aws/vpc/develop"] + } + } +} diff --git a/aws/vpc/release-please-manifest.json b/aws/vpc/release-please-manifest.json new file mode 100644 index 00000000..14b91f7e --- /dev/null +++ b/aws/vpc/release-please-manifest.json @@ -0,0 +1,3 @@ +{ + "aws/vpc": "0.1.0" +} diff --git a/docs/superpowers/plans/2026-10-24-terragrunt-production-release.md b/docs/superpowers/plans/2026-10-24-terragrunt-production-release.md new file mode 100644 index 00000000..6d6fbb30 --- /dev/null +++ b/docs/superpowers/plans/2026-10-24-terragrunt-production-release.md @@ -0,0 +1,580 @@ +# Terragrunt Production Release Gate Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** `aws/` の terragrunt stack で、`apply_trigger: merge` の環境は main merge で、`apply_trigger: release` の環境はサービスごとの release 公開で apply されるようにする。 + +**Architecture:** 環境ごとの apply 契機を `workflow-config.yaml` の環境属性 `apply_trigger` で持つ。main push の deploy-trigger は `merge` の target にだけ apply を割り当てる。release 側は `scripts/terragrunt-release/` の bash スクリプトで tag / branch からサービスを求め、`release` の環境を target にして既存の `reusable--terragrunt-executor.yaml` を呼ぶ。release-please は monorepo と同じく `aws//` ごとの設定ファイルで動かす。 + +**Tech Stack:** GitHub Actions、panicboat/deploy-actions v2.0.1、release-please-action v5、bash + yq(mikefarah v4)+ jq、actionlint + +**Spec:** `docs/superpowers/specs/2026-10-24-terragrunt-production-release-design.md` + +## Global Constraints + +- commit は `git commit -s`。`Co-Authored-By` を付けない +- commit message・コード内コメントは英語。コメントは 1 行で、Why not だけを書く。現在のタスクへの言及を書かない +- README は見出しが英語。`README-ja.md` の本文は日本語 +- GitHub Actions はすべて 40 文字の SHA で固定し、末尾に `# vX.Y.Z` を付ける(`lint-actions.yml` が検査する)。新しい action を追加しない(既存 workflow で使っている SHA だけを使う) +- workflow に環境名(`production`・`develop` など)を書かない。環境の判定は `apply_trigger` の値だけで行う +- `apply_trigger` の値は `merge` か `release` のどちらか +- release tag の形式は `-vX.Y.Z`、release PR の branch は `release-please--branches--main--components--` +- 対象は `aws/` だけ。`github/` と kubernetes の挙動を変えない +- 新しい依存(bats など)を追加しない。テストは plain bash で書く + +## Review Focus + +- この実装 PR 自体が `aws//release-please-config.json` を追加するので、Task 3 の検出で 12 サービスに `deploy:` が付く。merge すると `master` / `develop` の stack(`github-oidc-auth` など)が apply される。merge 前に plan を確認し、apply したくなければ最後の push の後にラベルを外す(Task 8) +- `aws/` の module 変更はこれまで apply されていなかったので、`master` の stack(`cost-management`・`route53`)には未 apply の差分が溜まっている可能性がある。最初の自動 apply にそれが含まれる(Task 8 で plan を確認する) +- `github-environment` が空のとき、executor の job が environment なしで今までどおり動くこと(Task 4。`environment:` に空文字を渡す挙動は ASSUMED なので、Task 8 の PR 上の plan で確かめる) +- `terragrunt-shared` stack の target が `deploy-kubernetes` の matrix に流れると、`kubernetes/manifests/null/` で kustomize が失敗する(Task 3 で matrix から除外し、Task 8 で aws と kubernetes を同時に触る PR の挙動を確かめる) +- tag や branch が形式に合わない、またはサービスに `release` 環境のディレクトリがないときは、apply せずに失敗すること(Task 4 のテスト) + +--- + +### Task 1: `apply_trigger` の検証スクリプトとテスト基盤 + +**Files:** +- Create: `scripts/terragrunt-release/validate-apply-trigger.sh` +- Create: `scripts/terragrunt-release/test.sh` +- Create: `.github/workflows/test-terragrunt-release.yaml` + +**Interfaces:** +- Produces: `bash scripts/terragrunt-release/validate-apply-trigger.sh [config_path]`(既定 `workflow-config.yaml`)。`name: terragrunt` の stack の全環境で `apply_trigger` が `merge` か `release` なら exit 0。そうでなければ環境ごとに `::error::stacks[].environments..apply_trigger must be merge or release (got '')` を stderr に出して exit 1 +- Produces: `bash scripts/terragrunt-release/test.sh`。`test_` で始まる関数をすべて実行し、1 つでも失敗すれば exit 1。各テストは `mktemp -d` の fixture ディレクトリに `cd` して実行する。helper は `assert_eq ` と `assert_fails ` + +- [ ] **Step 1: テストを書く** + +`test.sh` に helper と次のテストを書く。fixture の `workflow-config.yaml` は heredoc で作る。 + +```bash +test_validate_accepts_merge_and_release() # master: merge, production: release → exit 0 +test_validate_rejects_missing_trigger() # production に apply_trigger なし → exit 1、stderr に "environments.production.apply_trigger" +test_validate_rejects_unknown_value() # apply_trigger: relase → exit 1、stderr に "(got 'relase')" +test_validate_ignores_non_terragrunt_stacks() # name: kubernetes の stack に environments がなくても exit 0 +``` + +- [ ] **Step 2: テストが失敗することを確認する** + +Run: `bash scripts/terragrunt-release/test.sh` +Expected: FAIL(`validate-apply-trigger.sh` が存在しない) + +- [ ] **Step 3: `validate-apply-trigger.sh` を実装する** + +`set -euo pipefail`。`yq -o=json` で config を JSON にし、`jq` で `.stacks | to_entries[] | select(.value.name == "terragrunt")` の各環境を検査する。 + +- [ ] **Step 4: テストが通ることを確認する** + +Run: `bash scripts/terragrunt-release/test.sh` +Expected: 全テスト PASS、exit 0 + +- [ ] **Step 5: CI workflow を追加する** + +`test-terragrunt-release.yaml` は `on: pull_request` と `push: branches: [main]`、`permissions: contents: read`。job は `actions/checkout`(既存と同じ SHA)の後に `bash scripts/terragrunt-release/test.sh` を実行する。Task 2・Task 5 でこの job に検証コマンドを足す。 + +Run: `actionlint .github/workflows/test-terragrunt-release.yaml` +Expected: 出力なし、exit 0 + +- [ ] **Step 6: commit する** + +```bash +git add scripts/terragrunt-release .github/workflows/test-terragrunt-release.yaml +git commit -s -m "ci: validate apply_trigger in workflow-config" +``` + +--- + +### Task 2: 環境の宣言と main merge の apply を `apply_trigger` で絞る + +**Files:** +- Modify: `workflow-config.yaml` +- Modify: `.github/workflows/auto-label--deploy-trigger.yaml` +- Modify: `.github/workflows/test-terragrunt-release.yaml` +- Modify: `scripts/terragrunt-release/test.sh` + +**Interfaces:** +- Consumes: Task 1 の `validate-apply-trigger.sh` +- Produces: resolver の target に `apply_trigger` キーが入る(`matrix.target.apply_trigger`) + +- [ ] **Step 1: リポジトリの config を検査するテストを書く** + +`test.sh` に追加する。fixture ではなくリポジトリの `workflow-config.yaml` を読む(`$REPO_ROOT` を `git rev-parse --show-toplevel` で求める)。 + +```bash +test_repository_config_is_valid() # validate-apply-trigger.sh "$REPO_ROOT/workflow-config.yaml" が exit 0 +test_repository_config_never_merges_production() # terragrunt stack の production.apply_trigger == "release" +``` + +- [ ] **Step 2: テストが失敗することを確認する** + +Run: `bash scripts/terragrunt-release/test.sh` +Expected: FAIL(`master` に `apply_trigger` がない) + +- [ ] **Step 3: `workflow-config.yaml` の terragrunt stack に環境を宣言する** + +- `master`:既存の値に `apply_trigger: merge` を足す +- `develop`:`aws_region: ap-northeast-1`、`iam_role_plan: arn:aws:iam::270242382571:role/github-oidc-auth-develop-github-actions-plan-role`、`iam_role_apply: arn:aws:iam::270242382571:role/github-oidc-auth-develop-github-actions-apply-role`、`apply_trigger: merge` +- `production`:`aws_region: ap-northeast-1`、`iam_role_plan: arn:aws:iam::337169763788:role/github-oidc-auth-production-github-actions-plan-role`、`iam_role_apply: arn:aws:iam::337169763788:role/github-oidc-auth-production-github-actions-apply-role`、`apply_trigger: release` +- `aws_region` は stack の state bucket の region(`aws/*/root.hcl` の `remote_state.config.region`)に揃える。`aws/github-oidc-auth/develop/env.hcl` の `us-east-1` には合わせない(実装時の判断として PR description に書く) + +- [ ] **Step 4: develop と production の role が実在することをユーザーに確認してもらう** + +ユーザーに次のコマンドの実行を依頼し、4 つとも `RoleName` が返ることを確認する。返らなければ作業を止めて報告する。 + +```bash +aws iam get-role --role-name github-oidc-auth-develop-github-actions-plan-role --query Role.Arn # develop account +aws iam get-role --role-name github-oidc-auth-develop-github-actions-apply-role --query Role.Arn +aws iam get-role --role-name github-oidc-auth-production-github-actions-plan-role --query Role.Arn # production account +aws iam get-role --role-name github-oidc-auth-production-github-actions-apply-role --query Role.Arn +``` + +- [ ] **Step 5: deploy-trigger の apply 割り当てと検証を変える** + +- `deploy-trigger` job の先頭に `actions/checkout`(既存 SHA)と `bash scripts/terragrunt-release/validate-apply-trigger.sh` を足す。`Label Resolver` より前に置く +- `deploy-terragrunt` の `action-type` を次にする + +```yaml +action-type: ${{ matrix.target.stack == 'terragrunt' && (github.event_name == 'pull_request' && 'plan' || (matrix.target.apply_trigger == 'merge' && 'apply' || '')) || '' }} +``` + +- [ ] **Step 6: CI に repo config の検証を足し、テストと lint を通す** + +`test-terragrunt-release.yaml` の job に `bash scripts/terragrunt-release/validate-apply-trigger.sh` を足す。 + +Run: `bash scripts/terragrunt-release/test.sh && bash scripts/terragrunt-release/validate-apply-trigger.sh && actionlint` +Expected: 全テスト PASS、actionlint の出力なし + +Run(deploy-actions の config 検証。セッションの scratchpad に v2.0.1 の worktree を作る): +```bash +git -C /Users/takanokenichi/GitHub/panicboat/deploy-actions worktree add "$SCRATCH/deploy-actions-v2" 1ab520e5ef33b8d744982762ffc4e08009a34744 +(cd "$SCRATCH/deploy-actions-v2/action-scripts" && bundle install --quiet && \ + WORKFLOW_CONFIG_PATH="$PWD_REPO/workflow-config.yaml" SOURCE_REPO_PATH="$PWD_REPO" \ + bundle exec ruby config-manager/bin/config-manager validate) +``` +Expected: 検証成功(エラーなし)。`$SCRATCH` はセッションの scratchpad、`$PWD_REPO` はこの worktree の絶対パス + +- [ ] **Step 7: commit する** + +```bash +git add workflow-config.yaml .github/workflows scripts/terragrunt-release/test.sh +git commit -s -m "ci: apply only merge-triggered environments on push to main" +``` + +--- + +### Task 3: 全環境に効くファイルの変更を検出する + +**Files:** +- Modify: `workflow-config.yaml` +- Modify: `.github/workflows/auto-label--deploy-trigger.yaml` +- Modify: `.github/workflows/auto-label--label-dispatcher.yaml` + +**Interfaces:** +- Produces: stack `name: terragrunt-shared`(`paths: ["aws/{service}"]`、`attributes: {}`)。resolver はこの stack の target を `stack: terragrunt-shared`、`environment: null`、`working_directory: aws/` で出す。どの job もこの target を実行しない + +- [ ] **Step 1: 検出されないことを確認する(失敗の確認)** + +`aws/vpc/modules/main.tf` の末尾に空行を足し(commit しない)、v2.0.1 の dispatcher で作業ツリーの変更から検出結果を見る(Task 2 Step 6 の worktree を使う)。作業ツリーの変更が検出対象にならない場合は、一時 commit を作って `--base-ref HEAD~1 --head-ref HEAD` を付ける。 + +```bash +(cd "$SCRATCH/deploy-actions-v2/action-scripts" && \ + WORKFLOW_CONFIG_PATH="$PWD_REPO/workflow-config.yaml" SOURCE_REPO_PATH="$PWD_REPO" \ + bundle exec ruby label-dispatcher/bin/dispatcher test) +``` +Expected: `SERVICES_DETECTED` に `vpc` が含まれない + +- [ ] **Step 2: `terragrunt-shared` stack を追加する** + +`workflow-config.yaml` の `stacks` に、terragrunt stack の後へ次を足す。 + +```yaml + - name: terragrunt-shared + paths: + - "aws/{service}" + attributes: {} +``` + +- [ ] **Step 3: 検出されることを確認する** + +Run: Step 1 と同じコマンド +Expected: `SERVICES_DETECTED: ["vpc"]`、`DEPLOY_LABELS` に `deploy:vpc`。確認後、`git checkout -- aws/vpc/modules/main.tf` で空行を戻す(一時 commit を作った場合は、`git log -1` で一時 commit であることを確かめてから `git reset --hard HEAD~1`) + +- [ ] **Step 4: `terragrunt-shared` の target を実行 job から外す** + +`auto-label--deploy-trigger.yaml` の `deploy-terragrunt` と `deploy-kubernetes` の matrix に `exclude: - target: { stack: terragrunt-shared }` を足す(`deploy-kubernetes` は既存の `stack: terragrunt` の exclude に並べる)。 + +- [ ] **Step 5: release PR でラベル付与を走らせない** + +`auto-label--label-dispatcher.yaml` の `dispatch-labels` job に `if: ${{ !startsWith(github.head_ref, 'release-please--') }}` を足す。release-please が書き込む `aws//CHANGELOG.md` が `terragrunt-shared` に一致して全環境の plan が走るのを防ぐため。 + +- [ ] **Step 6: lint と config 検証を通す** + +Run: `actionlint && bash scripts/terragrunt-release/test.sh` と Task 2 Step 6 の config-manager validate +Expected: すべて成功 + +- [ ] **Step 7: commit する** + +```bash +git add workflow-config.yaml .github/workflows +git commit -s -m "ci: detect changes to files shared across terragrunt environments" +``` + +--- + +### Task 4: release の target を求めるスクリプトと executor の入力 + +**Files:** +- Create: `scripts/terragrunt-release/release-service.sh` +- Create: `scripts/terragrunt-release/release-targets.sh` +- Modify: `scripts/terragrunt-release/test.sh` +- Modify: `.github/workflows/reusable--terragrunt-executor.yaml` + +**Interfaces:** +- Produces: `bash scripts/terragrunt-release/release-service.sh --tag ` → stdout に ``。`^([a-z0-9-]+)-v[0-9]+\.[0-9]+\.[0-9]+$` に合わなければ `::error::` を出して exit 1 +- Produces: `bash scripts/terragrunt-release/release-service.sh --branch ` → `release-please--branches--main--components--` から `` を出す。合わなければ exit 1 +- Produces: `bash scripts/terragrunt-release/release-targets.sh [config_path]` → cwd をリポジトリルートとみなし、terragrunt stack の `apply_trigger: release` の環境のうち `aws//` が存在するものを、compact JSON 配列で stdout に出す。要素のキーは `service`・`environment`・`working_directory`・`aws_region`・`iam_role_plan`・`iam_role_apply`。1 つもなければ `::error::no release targets for ` を出して exit 1 +- Produces: executor の optional input `ref`(既定 `''`、checkout の `ref:` に渡す)と `github-environment`(既定 `''`、job の `environment:` に渡す) + +- [ ] **Step 1: テストを書く** + +```bash +test_release_service_parses_tag() # eks-holmesgpt-v1.2.3 → "eks-holmesgpt" +test_release_service_rejects_root_tag() # v0.1.0 → exit 1 +test_release_service_rejects_flux_tag() # kubernetes-production → exit 1 +test_release_service_parses_branch() # release-please--branches--main--components--vpc → "vpc" +test_release_service_rejects_root_branch() # release-please--branches--main → exit 1 +test_release_targets_lists_release_envs() # master: merge / production: release、aws/vpc/{master,production} がある → [{"service":"vpc","environment":"production","working_directory":"aws/vpc/production",...}] の 1 要素だけ +test_release_targets_skips_missing_dirs() # release 環境が 2 つ、ディレクトリは片方だけ → 1 要素 +test_release_targets_fails_without_targets() # aws/route53/master だけ → exit 1、stderr に "no release targets for route53" +``` + +- [ ] **Step 2: テストが失敗することを確認する** + +Run: `bash scripts/terragrunt-release/test.sh` +Expected: 追加したテストが FAIL + +- [ ] **Step 3: 2 つのスクリプトを実装する** + +`release-targets.sh` は `yq -o=json` + `jq` で環境を抜き出し、ディレクトリの存在を bash で確かめてから `jq -c -s` で配列にする。 + +- [ ] **Step 4: テストが通ることを確認する** + +Run: `bash scripts/terragrunt-release/test.sh` +Expected: 全テスト PASS + +- [ ] **Step 5: executor に入力を足す** + +`reusable--terragrunt-executor.yaml` に `ref` と `github-environment` を `required: false`、`default: ''` で足す。`execute-terragrunt` job に `environment: ${{ inputs.github-environment }}`、`Checkout` step に `with: ref: ${{ inputs.ref }}` を足す。既存の呼び出し元は変更しない。 + +Run: `actionlint` +Expected: 出力なし + +- [ ] **Step 6: commit する** + +```bash +git add scripts/terragrunt-release .github/workflows/reusable--terragrunt-executor.yaml +git commit -s -m "ci: resolve release targets from workflow-config" +``` + +--- + +### Task 5: サービスごとの release-please 設定 + +**Files:** +- Create: `aws//release-please-config.json` と `aws//release-please-manifest.json`(`release` 環境のディレクトリを持つ全サービス) +- Create: `scripts/terragrunt-release/check-release-configs.sh` +- Modify: `scripts/terragrunt-release/test.sh` +- Modify: `.github/workflows/release.yml` +- Modify: `.github/workflows/test-terragrunt-release.yaml` + +**Interfaces:** +- Consumes: Task 4 の `release-targets.sh`(`release` 環境を持つサービスの判定に使う) +- Produces: `bash scripts/terragrunt-release/check-release-configs.sh [config_path]` → cwd をリポジトリルートとみなす。`release-targets.sh` が成功する `aws/*` のサービスの集合と、`aws/*/release-please-config.json` を持つサービスの集合が一致し、各 config の `packages["aws/"].component == ""`、各 manifest に `"aws/"` キーがあれば exit 0。不一致はサービスごとに `::error::` を出して exit 1 + +- [ ] **Step 1: テストを書く** + +```bash +test_check_configs_accepts_matching_set() # aws/vpc/production と aws/vpc/release-please-{config,manifest}.json → exit 0 +test_check_configs_rejects_missing_config() # aws/eks/production があり config がない → exit 1、stderr に "eks" +test_check_configs_rejects_orphan_config() # aws/route53/master だけで config がある → exit 1、stderr に "route53" +test_check_configs_rejects_wrong_component() # component が "eks" の aws/vpc config → exit 1 +``` + +- [ ] **Step 2: テストが失敗することを確認する** + +Run: `bash scripts/terragrunt-release/test.sh` +Expected: 追加したテストが FAIL + +- [ ] **Step 3: `check-release-configs.sh` を実装し、テストを通す** + +Run: `bash scripts/terragrunt-release/test.sh` +Expected: 全テスト PASS + +- [ ] **Step 4: リポジトリで検査が失敗することを確認する** + +Run: `bash scripts/terragrunt-release/check-release-configs.sh` +Expected: exit 1。不足として 12 サービス(`alb`・`eks`・`eks-holmesgpt`・`eks-karpenter`・`eks-logs`・`eks-metrics`・`eks-secrets`・`eks-traces`・`github-oidc-auth`・`iam-service-linked-roles`・`secrets-manager`・`vpc`)が出る。一覧が違えば実態を優先し、PR description に差分を書く + +- [ ] **Step 5: 設定ファイルを生成する** + +`git fetch origin main && git merge-base HEAD origin/main` の SHA を `bootstrap-sha` に使う。各サービスの config は次の形(`` を置き換える)。manifest は `{"aws/": "0.1.0"}`。 + +```json +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "bootstrap-sha": "", + "separate-pull-requests": true, + "label": "autorelease: pending,release: ", + "changelog-sections": [ + { "type": "feat", "section": "Features" }, + { "type": "fix", "section": "Bug Fixes" }, + { "type": "perf", "section": "Performance Improvements" }, + { "type": "revert", "section": "Reverts" }, + { "type": "refactor", "section": "Code Refactoring" }, + { "type": "chore", "section": "Miscellaneous Chores" }, + { "type": "docs", "section": "Documentation", "hidden": true }, + { "type": "style", "section": "Styles", "hidden": true }, + { "type": "test", "section": "Tests", "hidden": true }, + { "type": "build", "section": "Build System", "hidden": true }, + { "type": "ci", "section": "Continuous Integration", "hidden": true } + ], + "packages": { + "aws/": { + "release-type": "simple", + "component": "", + "include-component-in-tag": true + } + } +} +``` + +Run: `bash scripts/terragrunt-release/check-release-configs.sh` +Expected: exit 0 + +- [ ] **Step 6: `release.yml` を設定ファイルの探索方式に置き換える** + +monorepo の `.github/workflows/release.yml` と同じ構成にする(`discover` job が `git ls-files '*/release-please-config.json'` で path を集め、`release-please` job が matrix で `config-file` / `manifest-file` を渡す。`workflow_dispatch: {}` と `concurrency: release-please` も同じ)。action の SHA は platform の既存のものを使う。`test-terragrunt-release.yaml` の job に `bash scripts/terragrunt-release/check-release-configs.sh` を足す。 + +Run: `actionlint && bash scripts/terragrunt-release/test.sh` +Expected: すべて成功 + +- [ ] **Step 7: commit する** + +```bash +git add aws/*/release-please-config.json aws/*/release-please-manifest.json scripts/terragrunt-release .github/workflows +git commit -s -m "ci: release each terragrunt service with its own release-please config" +``` + +--- + +### Task 6: release PR に plan を出す + +**Files:** +- Create: `.github/workflows/auto-release--plan.yaml` + +**Interfaces:** +- Consumes: Task 4 の `release-service.sh --branch`、`release-targets.sh`、executor + +- [ ] **Step 1: workflow を書く** + +- `on: pull_request: types: [opened, synchronize, reopened]` +- `resolve` job:`if: startsWith(github.head_ref, 'release-please--branches--main--components--')`。checkout の後、`release-service.sh --branch "$HEAD_REF"`(`HEAD_REF` は `env:` で渡す)と `release-targets.sh` の結果を output `targets` にする +- `plan` job:`needs: resolve`、matrix `target: ${{ fromJson(needs.resolve.outputs.targets) }}`、`fail-fast: false`。`reusable--terragrunt-executor.yaml` を `action-type: plan`、`iam-role: matrix.target.iam_role_plan`、`aws-region`・`working-directory`・`service-name`・`environment` は target から渡す。`ref` と `github-environment` は渡さない +- `permissions`:`id-token: write`、`contents: read`、`pull-requests: write` +- plan の出し直しは、この workflow run の「Re-run all jobs」で行う(README に書く。Task 7) + +- [ ] **Step 2: lint を通す** + +Run: `actionlint .github/workflows/auto-release--plan.yaml` +Expected: 出力なし + +- [ ] **Step 3: commit する** + +```bash +git add .github/workflows/auto-release--plan.yaml +git commit -s -m "ci: plan release environments on release pull requests" +``` + +--- + +### Task 7: release 公開で apply する + +**Files:** +- Create: `.github/workflows/auto-release--trigger.yaml` +- Modify: `README.md` +- Modify: `README-ja.md` + +**Interfaces:** +- Consumes: Task 4 の `release-service.sh --tag`、`release-targets.sh`、executor の `ref` / `github-environment` + +- [ ] **Step 1: workflow を書く** + +- `on: release: types: [published]` と `workflow_dispatch: inputs: tag`(`required: true`、`type: string`、description `Release tag to (re-)apply (e.g. vpc-v0.2.0)`) +- `concurrency: group: auto-release-trigger-${{ github.event.release.tag_name || inputs.tag }}`、`cancel-in-progress: false` +- `resolve` job:`TAG` を `env:` で渡し、tag を ref にして checkout してから `release-service.sh --tag "$TAG"` と `release-targets.sh` を実行し、output `targets` と `tag` を出す +- `apply` job:matrix で executor を `action-type: apply`、`iam-role: matrix.target.iam_role_apply`、`ref: needs.resolve.outputs.tag`、`github-environment: matrix.target.environment` で呼ぶ +- `permissions`:`id-token: write`、`contents: read`、`pull-requests: write` +- GitHub environment `production` は job の宣言で GitHub が自動で作るものを使い、Terraform(`github/repository`)では管理しない(required reviewers を付けないので、管理する設定がない) + +- [ ] **Step 2: lint を通す** + +Run: `actionlint .github/workflows/auto-release--trigger.yaml` +Expected: 出力なし + +- [ ] **Step 3: README を更新する** + +`README.md` と `README-ja.md` の `## 🚢 Deployment` を更新する。 +- `### Trigger` に、`apply_trigger: merge` の環境は main push で、`apply_trigger: release` の環境は `aws/` の release 公開で apply されることを書く +- `### Environments and authentication` に `apply_trigger` の意味と値を書く +- release PR の plan の出し直しは `auto-release--plan` の run の「Re-run all jobs」、apply の再実行は `auto-release--trigger` の `workflow_dispatch` に tag を渡すことを書く +- `Pipeline Flow` の mermaid に `release published → auto-release--trigger → terragrunt apply` を足す + +- [ ] **Step 4: commit する** + +```bash +git add .github/workflows/auto-release--trigger.yaml README.md README-ja.md +git commit -s -m "ci: apply release environments when a service release is published" +``` + +--- + +### Task 8: PR 作成と E2E 確認 + +ユーザーと一緒に行う。各 Step の結果を PR description の Test plan に記録する。 + +- [ ] **Step 1: 全体の検証を通す** + +Run: `bash scripts/terragrunt-release/test.sh && bash scripts/terragrunt-release/validate-apply-trigger.sh && bash scripts/terragrunt-release/check-release-configs.sh && actionlint` +Expected: すべて成功 + +- [ ] **Step 2: push して Draft PR を作る** + +`git push -u origin HEAD`。`.github/pull_request_template.md` の有無を確認し、あればその構成で本文を書く。`gh pr create --draft`。タイトルは英語(例:`ci: gate production terragrunt applies behind service releases`)。本文に Review Focus の 1・2 番目を書く。 + +- [ ] **Step 3: PR 上の plan を確認する** + +- 12 サービスに `deploy:` が付き、`master` / `develop` / `production` のうち存在する環境で plan が走ることを確認する +- plan job が environment なしで動いていること(`github-environment` が空)を確認する +- `master`(`github-oidc-auth`)と `develop` の plan の差分をユーザーに見せ、merge で apply してよいか確認する。してほしくなければ、最後の push の後にラベルを外す +- open のままの release PR #890 を close してよいかユーザーに確認し、了承されたら `gh pr close 890` を実行する + +- [ ] **Step 4: merge 後の確認(ユーザーが merge した後)** + +- main push の deploy-trigger で production の apply job が skip されていること +- 次に `aws//modules/` を変える PR で、ラベルの自動付与、全環境の plan、merge 後の `merge` 環境だけの apply、`aws/` の release PR の作成、release PR への production の plan を順に確認する +- release PR の merge で production だけが承認待ちなしに apply されること、`workflow_dispatch` に同じ tag を渡して再実行できることを確認する +- aws と kubernetes を同時に変える PR で、`deploy-kubernetes` が `terragrunt-shared` の target を実行しないことを確認する +- `aws/github-oidc-auth/develop/` だけを変える PR を merge しても、`github-oidc-auth` の release PR ができないことを確認する(Task 9) + +--- + +### Task 9: merge 環境だけの変更を release から外す + +**Files:** +- Modify: `scripts/terragrunt-release/check-release-configs.sh` +- Modify: `scripts/terragrunt-release/test.sh` +- Modify: `aws//release-please-config.json`(12 サービスすべて) +- Modify: `README.md`・`README-ja.md`(`## 🚢 Deployment` の `### Trigger`) + +**Interfaces:** +- Consumes: Task 5 の `check-release-configs.sh` と `write_service_release_config`(test.sh) +- Produces: `check-release-configs.sh` は、release 環境を持つ各サービスについて、`packages["aws/"]["exclude-paths"]` を順序を無視して期待値と比べる。期待値は、`workflow-config.yaml` で `name: terragrunt` の stack にある `apply_trigger: merge` の全環境の `aws//` で、ディレクトリの有無は問わない。一致しなければ `::error::aws/: exclude-paths must be (got )` を出して exit 1。`` と `` は 1 行の JSON 配列で、`exclude-paths` がないときの `` は `null` + +- [ ] **Step 1: テストを書く** + +`write_service_release_config` は既定で `"exclude-paths":["aws//master"]` を書く(`write_release_config` の merge 環境が `master` だけなので)。既存のテストは変えずに通ること。 + +```bash +test_check_configs_rejects_missing_exclude_paths() # exclude-paths を消した aws/vpc config → exit 1、stderr に 'aws/vpc: exclude-paths must be ["aws/vpc/master"] (got null)' +test_check_configs_rejects_relative_exclude_paths() # exclude-paths が ["master"] → exit 1、stderr に 'aws/vpc: exclude-paths must be ["aws/vpc/master"]' +test_check_configs_accepts_exclude_paths_in_any_order() # merge 環境が master と develop の config で、exclude-paths が ["aws/vpc/develop","aws/vpc/master"] → 出力なしで exit 0 +``` + +- [ ] **Step 2: テストが失敗することを確認する** + +Run: `bash scripts/terragrunt-release/test.sh` +Expected: 追加した 2 つの rejects テストが FAIL + +- [ ] **Step 3: `check-release-configs.sh` に検査を足し、テストを通す** + +Run: `bash scripts/terragrunt-release/test.sh` +Expected: 全テスト PASS + +- [ ] **Step 4: リポジトリで検査が失敗することを確認する** + +Run: `bash scripts/terragrunt-release/check-release-configs.sh` +Expected: exit 1。12 サービスすべてについて exclude-paths のエラーが出る + +- [ ] **Step 5: 12 サービスの config に `exclude-paths` を足す** + +各 `aws//release-please-config.json` の `packages["aws/"]` に `"exclude-paths": ["aws//master", "aws//develop"]` を足す。既存のキーの順序と 2 スペースのインデントを保つ。 + +Run: `bash scripts/terragrunt-release/check-release-configs.sh && bash scripts/terragrunt-release/test.sh` +Expected: どちらも成功 + +- [ ] **Step 6: README に動作を書く** + +`### Trigger` の release-please 設定の項目の下に、`apply_trigger: merge` の環境ディレクトリだけを変える commit は `exclude-paths` で release から外れること、`check-release-configs.sh` がその一覧を `workflow-config.yaml` と照合することを書く。`README-ja.md` も同じ内容を日本語で書く。 + +- [ ] **Step 7: commit する** + +```bash +git add scripts/terragrunt-release aws/*/release-please-config.json README.md README-ja.md +git commit -s -m "ci: keep merge-only environment changes out of service releases" +``` + +--- + +### Task 10: production を除外環境にする + +**Files:** +- Modify: `workflow-config.yaml` +- Modify: `scripts/terragrunt-release/release-targets.sh` +- Modify: `scripts/terragrunt-release/test.sh` +- Modify: `.github/workflows/auto-release--plan.yaml` +- Modify: `.github/workflows/auto-release--trigger.yaml` +- Modify: `README.md`・`README-ja.md`(`## 🚢 Deployment` の `### Trigger`) + +**Interfaces:** +- Consumes: Task 4 の `release-targets.sh`、Task 5・9 の `check-release-configs.sh`(変更しない) +- Produces: `release-targets.sh [config]` は、`name: terragrunt` の stack の `exclude` の各 rule(キーは `service` と `environment`。rule のキーがすべて一致したら除外。deploy-actions の `excluded?` と同じ意味)に一致する target を外す。`apply_trigger: release` の環境のディレクトリが 1 つもなければ今までどおり `::error::no release targets for ` で exit 1。ディレクトリはあるが全部除外されたら `[]` を出して exit 0。`exclude` がない stack は今までどおり + +- [ ] **Step 1: テストを書く** + +```bash +test_release_targets_skips_excluded_envs() # write_release_config に exclude [{environment: production}] を足し、aws/vpc/production と aws/vpc/staging → staging だけの配列 +test_release_targets_returns_empty_when_all_excluded() # exclude [{environment: production}, {environment: staging}]、aws/vpc/production だけ → stdout が "[]"、exit 0 +test_release_targets_honors_service_scoped_exclude() # exclude [{service: vpc, environment: production}]、aws/vpc/production と aws/eks/production → vpc は "[]"、eks は production の 1 件 +test_check_configs_requires_config_for_excluded_env() # exclude [{environment: production}]、aws/eks/production があり config がない → exit 1、stderr に "aws/eks/release-please-config.json"。config を足すと出力なしで exit 0 +test_release_workflows_skip_empty_targets() # auto-release--plan.yaml の jobs.plan.if と auto-release--trigger.yaml の jobs.apply.if が "needs.resolve.outputs.targets != '[]'" +test_repository_config_excludes_production() # リポジトリの workflow-config.yaml の terragrunt stack の exclude に {environment: production} がある +``` + +- [ ] **Step 2: テストが失敗することを確認する** + +Run: `bash scripts/terragrunt-release/test.sh` +Expected: 追加したテストのうち、`test_check_configs_requires_config_for_excluded_env` 以外が FAIL + +- [ ] **Step 3: `release-targets.sh` に除外を足し、2 つの workflow の job に `if: needs.resolve.outputs.targets != '[]'` を付け、`workflow-config.yaml` の terragrunt stack の末尾に `exclude: [{environment: production}]` を足す** + +`workflow-config.yaml` の `exclude` は monorepo と同じ block style で書き、`production` の環境定義は残す(deploy-actions は `exclude` の環境が `environments` に宣言されていることを要求する)。 + +Run: `bash scripts/terragrunt-release/test.sh && bash scripts/terragrunt-release/validate-apply-trigger.sh && bash scripts/terragrunt-release/check-release-configs.sh && actionlint` +Expected: test・validate・check は成功。actionlint は既存の `target-environment` の 1 件だけ + +- [ ] **Step 4: deploy-actions の config 検証を通す** + +deploy-actions v2.0.1(`1ab520e5ef33b8d744982762ffc4e08009a34744`)の `WorkflowConfig` で、リポジトリの `workflow-config.yaml` が検証を通り、`excluded?` が `aws/vpc/production` を除外することを確かめる(`.git` がファイルの worktree では動かないので、platform の通常 clone か `git archive` で取り出した tree で実行する)。 + +Expected: 検証エラーなし、production が excluded + +- [ ] **Step 5: README に動作を書く** + +`### Trigger` に、terragrunt stack の `exclude` に一致する環境は label 付与・PR の plan・main merge の apply・release の plan / apply のすべてから外れ、release PR はできるが merge しても apply しないことを書く。`README-ja.md` も同じ内容を日本語で書く。 + +- [ ] **Step 6: commit する** + +```bash +git add workflow-config.yaml scripts/terragrunt-release .github/workflows README.md README-ja.md +git commit -s -m "ci: exclude production from platform terragrunt runs" +``` diff --git a/docs/superpowers/specs/2026-10-24-terragrunt-production-release-design.md b/docs/superpowers/specs/2026-10-24-terragrunt-production-release-design.md new file mode 100644 index 00000000..74434973 --- /dev/null +++ b/docs/superpowers/specs/2026-10-24-terragrunt-production-release-design.md @@ -0,0 +1,197 @@ +# Terragrunt Production Release Gate Design + +> **Goal**: `aws/` 配下の terragrunt stack で、production 以外の環境は「PR で plan、main merge で apply」、production は「サービスごとの release PR の merge(release 公開)で apply」にする。module を修正したときに develop と production へ同時に apply される状態をなくし、develop で確認してから production に出せるようにする。 + +--- + +## Context + +### Why + +- テスト用に `develop` 環境を追加しようとした際に、今の CI では module を修正すると、その module を参照する全環境に同じ main merge で apply されることに気付いた +- develop で module の変更を確かめてから production に出す、という段階を踏めない。develop を作る意味がなくなる + +### Current state + +- `workflow-config.yaml` の terragrunt stack は `aws/{service}/{environment}` と `github/{service}/{environment}` を対象にし、宣言している環境は `master` だけ(VERIFIED) +- label-dispatcher(`panicboat/deploy-actions` v2.0.1)が付けるラベルは `deploy:` で、環境の情報を持たない(REASONED:`label-dispatcher/use_cases/detect_changed_services.rb`) +- label-resolver は `environments` input が未指定なら、宣言済みの全環境に展開する。PR では全環境で plan、main への push で全環境 apply になる(REASONED:`.github/workflows/auto-label--deploy-trigger.yaml` と resolver の README) +- `modules/` 配下の変更は環境名 `modules` として扱われ、宣言済みの環境と一致しないため**ラベルが付かない**(VERIFIED:#1100 は `github/repository/modules/main.tf` だけの変更で、dispatcher のログは `SERVICES_DETECTED: []`。`deploy:repository` は手動で付けられていた) + - 今は module を変えた PR にラベルを手動で付けて apply している +- `release.yml` はリポジトリ全体で 1 本の release-please(`release-type: simple`、non-manifest)を動かしている。release PR #890(`chore(main): release 1.0.0`)が open のまま残っている +- `monorepo` リポジトリでは、ディレクトリごとに `release-please-config.json` / `release-please-manifest.json` を置き、`release.yml` がそれを `git ls-files` で見つけて matrix で release-please を回している。release 公開を `auto-release--trigger.yaml` が受けて、tag 名 `-vX.Y.Z` から対象ディレクトリを決めている + +### Prior attempt (#886) + +- 2026-09-06 に #886「release-please phase 2 — production deploy trigger」を merge し、約 3 時間後に #889 で revert した。spec は #886 の commit(`7870e23`)の `docs/superpowers/specs/2026-09-06-release-please-phase2-deploy-design.md` にある +- 内容は今回と近い。aws の production を持つ 12 サービスを release-please の component にし、release 公開で production に apply する。加えて kubernetes を 1 component にし、Flux の追従先を `main` から `kubernetes-production` tag に切り替えていた +- revert の理由は記録がなく、ユーザーも覚えていない。ログから分かっていることは次のとおり + - merge 直後に作られた release PR は `kubernetes 0.2.0`(#887)だけで、aws の release → apply の経路は一度も動いていない + - サービス一覧のハードコードをやめる修正(#888)を作ったが、merge せずに close した +- 今回の設計は、そこから次の点を変えている + - kubernetes / Flux は対象外にする + - サービス一覧をハードコードせず、ディレクトリの存在から求める + - release-please の設定を monorepo と同じディレクトリごとの方式にする + - `chore` を CHANGELOG に表示し、`chore` だけの変更でも release PR ができるようにする(#886 の spec は「`chore` も patch bump されるので release PR に積まれる」としていたが、release-please は CHANGELOG に載る commit がなければ release PR を作らない。VERIFIED:`googleapis/release-please` `src/strategies/base.ts` の `changelogEmpty` で `No user facing commits found ... - skipping`) + +--- + +## Goals + +- module・`root.hcl` など、サービスの全環境に効くファイルの変更を自動で検出し、ラベルを手動で付けなくても plan / apply が走る +- production 以外の環境は、PR で plan、main merge で apply する +- production は main merge では apply しない。サービスごとの release PR を merge(release 公開)したときに、そのサービスの production だけを apply する +- 元の変更 PR でも production の plan を出す(apply はしない)。develop に入れる前に、production で何が起きるかを確認できる +- release PR でも production の plan を出し、merge の直前に確認できる +- release 時の apply が失敗したら、tag を指定して手動で再実行できる + +## Non-goals + +- kubernetes(`kubernetes/components/` と Flux)。apply の経路が terragrunt と違うため対象外 +- `github/` 配下。環境が `master` しかないため対象外 +- `master` 環境の扱いの変更。今までどおり PR で plan、main merge で apply する +- サービスをまたぐ依存の検出。例えば `aws/eks/modules/lookups.tf` は `../../vpc/lookup` を参照しているが、`aws/vpc/lookup` を変えても検出されるのは `vpc` だけで、`eks` は検出しない +- `develop` 環境の stack ディレクトリ(`aws//develop/`)の作成。この設計は CI の仕組みだけを扱う +- production apply の順序(vpc → eks など)の自動制御。どの release PR から merge するかで人が決める。今の main merge でも順序は保証されていない + +## Anti-goals + +- main merge で production に apply が走ること。どんな経路でも起きてはいけない +- 変更していない production に apply が走ること(plan だけなら許容する) +- release PR を merge しても、どのサービスの production にも apply されないこと +- `chore`・`refactor`・Renovate の変更など、main に入った変更が production に出る手段を持たないこと + +--- + +## Decisions + +ユーザーと合意した事項。 + +- **production 以外は PR で plan、merge で apply**: 現行の仕組みを維持する +- **production は release PR の merge で apply**: release PR の単位は `aws/` ごと +- **release PR の確認場所**: production の plan は release PR に出す +- **元の変更 PR でも production の plan を出す**: module の変更が production でだけ失敗するケースを develop に merge する前に見つけるため。release PR で初めて気付くと、問題のある変更がすでに main に入り、そのサービスの他の変更も production に出せなくなる +- **`chore` も release 対象にする**: `feat` / `fix` だけを対象にすると、Renovate の version bump(`chore(...)`)が production に出る手段を持たなくなる。人の PR も Renovate の PR もタイトルを変えずに使える代わりに、Renovate の version bump もすべて CHANGELOG とリリースに載る +- **`refactor` も release 対象にする**: このリポジトリでは module の変更に `refactor` も使う(例:#909)。`chore` と同じ理由で CHANGELOG に表示する +- **release 公開時の apply に承認(required reviewers)を付けない**: release PR の merge がすでに人の判断なので、承認を二重にしない +- **apply の契機は `workflow-config.yaml` の環境属性 `apply_trigger` で持つ**: 値は `merge` か `release`。deploy-actions を変更せずに済み、どの環境が release で apply されるかの情報が workflow に散らばらない +- **対象は `aws/` だけ**: `github/` は `master` しかなく、kubernetes は経路が違う +- **`apply_trigger: merge` の環境だけの変更では release PR を作らない**: production に変更がないのに no-op の apply が走るのを避ける。環境ディレクトリどうしは `modules/`・`root.hcl` を共有しているので、除外するのは merge 環境のディレクトリだけにする + +--- + +## Design + +### Environments + +| 環境 | PR | main merge | release 公開 | +|---|---|---|---| +| `master` | plan | apply | なし | +| `develop` | plan | apply | なし | +| `production` | plan | なし | apply | + +- 各環境の属性に `apply_trigger` を持たせ、表の「main merge」「release 公開」のどちらで apply するかを表す。`master` と `develop` は `merge`、`production` は `release` + - deploy-actions の config 検証は、最上位キー(`stacks` のみ)と stack のフィールドを固定しているが、環境の属性には任意のキーを許可し、resolver の出力(matrix の target)にそのまま入れる(REASONED:`deploy-actions` `shared/entities/workflow_config.rb`) + - deploy-actions はこの値の意味を知らないので、platform 側の workflow で `merge` / `release` 以外の値や未設定を失敗させる +- `workflow-config.yaml` の terragrunt stack に `develop` と `production` を宣言する + - `production`:`337169763788`、role は `github-oidc-auth-production-github-actions-{plan,apply}-role`(#886 の spec と過去のコメントアウトから。実装時に実在を確認する) + - `develop`:`270242382571`、role 名は同じ命名規則とし、実装時に実在を確認する。`aws/github-oidc-auth/develop/env.hcl` の `aws_region` は `us-east-1` なので、環境属性の `aws_region` をどうするかも実装時に確認する +- `production` しかないサービス(`aws/eks` など)も同じ扱いにする。どんな変更も release PR を通る +- 現時点の production は monorepo で管理しているので、terragrunt stack の `exclude` に `environment: production` を置き、platform からは plan も apply もしない(ユーザー決定。後で monorepo 側を破棄して platform で作り直すときに外す) + - deploy-actions は `exclude` の環境を label 付与と matrix から外す(VERIFIED:`shared/entities/workflow_config.rb` の `excluded?`)。`exclude` に書く環境は `environments` に宣言されている必要があるので、`production` の定義は残す + - `release-targets.sh` も同じ `exclude` を読み、除外された環境を target から外す。release 環境のディレクトリがあり全部除外された場合は `[]` を返して成功し、release PR の plan と release 公開の apply の job は skip する + - release-please の設定は残す(ユーザー決定)。除外中も release PR はでき、merge しても何も apply しない。`check-release-configs.sh` は除外された環境も release 環境として数え、設定を必須のままにする + +### Change detection + +- サービスの全環境に効くファイル(`modules/`・`lookup/`・`root.hcl`・`Makefile` など、環境ディレクトリの外にあるもの)の変更で `deploy:` が付くようにする +- 環境ディレクトリ内の変更(`aws/vpc/develop/...`)は、今までどおり `deploy:` になる +- ラベルはサービス単位のままなので、どちらの変更でも、そのサービスの全環境が plan の対象になる。環境ディレクトリだけの変更で他環境に no-op の plan が出るのは許容する +- 実装方法は builder に任せる。`workflow-config.yaml` だけで実現する案と、`deploy-actions` を変更する案がある + - config だけの案:例えば `aws/{service}` を `attributes: {}` を持つ別 stack として宣言すると、dispatcher はその配下の変更で `deploy:` を付ける(REASONED:dispatcher は `attributes` を持つ stack を `environment: null` で評価する)。ただし resolver がその stack の target(`environment: null`、`working_directory: aws/`)も出力するので、terragrunt の job から除外する必要がある + - release-please が書き込む `aws//CHANGELOG.md` やマニフェストの変更でも、`aws/{service}` 配下として検出されてしまう。release PR では通常のラベル付与を走らせない、または除外する必要がある + +### Pull request + +- 元の変更 PR では、ラベルの付いたサービスについて `master`・`develop`・`production` のうちディレクトリが存在する全環境で plan を出す(現行の resolver の挙動のまま) +- production の plan には今までどおり `iam_role_plan` を使う + +### Main merge + +- main への push では、`matrix.target.apply_trigger == 'merge'` の target にだけ `action-type: apply` を割り当てる。`release` の target は `action-type` を空にし、`reusable--terragrunt-executor.yaml` の `if: inputs.action-type != ''` で skip させる +- workflow に環境名(`production` など)を書かない。`release` の target に apply が割り当たらないことをテストで確かめる + +### Release PR + +- `apply_trigger: release` の環境のディレクトリ(今は `production/`)を持つ `aws/` 配下のサービスごとに、`aws//release-please-config.json` と `aws//release-please-manifest.json` を置く(monorepo の `dystopia/monolith/` と同じ形) + - 2026-10-24 時点の対象は `alb`・`eks`・`eks-holmesgpt`・`eks-karpenter`・`eks-logs`・`eks-metrics`・`eks-secrets`・`eks-traces`・`github-oidc-auth`・`iam-service-linked-roles`・`secrets-manager`・`vpc` の 12 サービス。実装時に `aws/*/production/` の存在で確認し直す + - `separate-pull-requests: true`、`include-component-in-tag: true`、`component: `。tag は `-vX.Y.Z` + - `changelog-sections` で `chore` と `refactor` を表示する。`feat` / `fix` / `perf` を上に、`refactor` と `chore` を下の別見出しに並べる + - `exclude-paths` に、`apply_trigger: merge` の全環境について `aws//` を並べる。ディレクトリがない環境も含める(後から環境を足したときに書き忘れないため) + - release-please v17.6.0 は commit のファイルパスを `exclude-paths` と前方一致で比べる(VERIFIED:`src/util/commit-exclude.ts`)。repo root からのパスで書かないと効かない + - commit が除外されるのは、そのサービス内で変えたファイルがすべて `exclude-paths` の下にあるときだけ。merge 環境と `modules/` を一緒に変えた commit は release に入る +- `release.yml` を monorepo と同じ「設定ファイルを探して matrix で回す」形に置き換える。今のリポジトリ全体の release-please はやめる +- release PR が開かれた・更新されたときに、そのサービスの `aws//production` だけで plan を出す +- release PR の plan を merge の直前に出し直す手段を用意する(ラベル・コメントなど、方式は builder に任せる)。release PR を作った後に手動変更(drift)があると、plan に出ていない差分も apply されるため + +### Release published + +- apply job は GitHub environment `production` を宣言する。production の apply role の trust が `main` への push か `environment:production` しか許可していないため、tag ref で動く release 公開の job はこの宣言がないと role を assume できない(VERIFIED:`aws/github-oidc-auth/modules/main.tf` の `apply_conditions`) + - environment `production` に required reviewers は付けない + - platform には GitHub environment がまだない。job が宣言すると GitHub が自動で作るが、Terraform(`github/repository`)で管理するかは builder に任せる +- release 公開で、tag 名から `` を取り出し、`workflow-config.yaml` で `apply_trigger: release` の環境について `aws//` が存在すれば、その commit で apply する。`iam_role_apply` と `aws_region` も同じ環境の属性から取る +- tag を指定して手動で再実行できる `workflow_dispatch` を用意する(monorepo の `auto-release--trigger.yaml` と同じ) +- tag の形式に合わない、またはディレクトリがない場合は失敗させる + +### Renovate + +- `renovate.json` は変更しない +- Renovate の PR は `chore(...)` のまま release PR に入る。`**/production/**` の PR は既に automerge が無効になっている + +--- + +## Constraints + +- `deploy-actions` は v2.0.1(SHA `1ab520e`)に固定されている。`deploy-actions` を変更する場合は別リポジトリでのリリースと、platform 側の SHA 更新が必要になる +- GitHub Actions はすべて SHA 固定(`lint-actions.yml` の `ensure-sha-pinned-actions` が検査する) +- release-please が release PR を作るのは GitHub App token なので、release PR でも `pull_request` workflow が起動する(monorepo と同じ) +- production の apply role の trust は `repo:panicboat/{monorepo,platform}:ref:refs/heads/main` と `environment:production` を許可し、plan role は `pull_request` と `main` を許可している。release PR の plan は `pull_request` なので今の plan role で通る +- tag `kubernetes-production` と `v0.1.0` が残っている。今回の tag(`-vX.Y.Z`)とは衝突しない + +--- + +## Left to the builder + +- 全環境に効くファイルの変更を検出する方法(config だけか、`deploy-actions` の変更か) +- release PR で通常のラベル付与・plan を走らせない方法 +- release PR の plan を出し直す手段の方式 +- 各サービスの初期バージョンと `bootstrap-sha`(最初の release PR に過去の全履歴が載らないようにする) +- open のまま残っている release PR #890 の close(実装時にユーザーに確認してから行う) + +--- + +## Calls made without the user + +ユーザーに確認せず私が決めたこと。 + +- CHANGELOG で `chore` を `feat` / `fix` の下の別見出しに分ける(「本当の変更を見逃しそう」という懸念への対策) +- release 時の apply が失敗したときに tag 指定で再実行できるようにする +- 環境ディレクトリだけの変更でも、そのサービスの全環境に plan を出す(no-op の plan は許容する) +- develop の `aws_region`、アカウント、plan/apply role の実在は、実装時に builder が AWS 側で確認する(`aws/github-oidc-auth/develop/env.hcl` は `us-east-1`) +- `github-oidc-auth` も release 対象に含める。この pipeline 自身が使う role を変えるリスクは今の main merge でも同じなので、扱いを変えない +- production の apply role の trust から `main` を外す強化は範囲に入れない。「main merge で production に apply しない」ことは workflow の分岐とテストで守る +- `docs` / `ci` など `feat` / `fix` / `perf` / `refactor` / `chore` 以外の type は、今までどおりリリースを作らず次のリリースに相乗りする + +--- + +## Verification + +- [ ] `aws//modules/` だけを変更した PR に、自動で `deploy:` が付く +- [ ] その PR で `master`・`develop`・`production` のうち存在する全環境の plan が出る +- [ ] その PR を merge すると、`apply_trigger: merge` の `master` と `develop` だけが apply され、production の apply job が起動しない +- [ ] `apply_trigger` が未設定、または `merge` / `release` 以外の値の環境があると、workflow が失敗する +- [ ] merge 後に `aws/` の release PR が作られる、または更新される。`chore` だけ、`refactor` だけの変更でも作られる +- [ ] release PR に、そのサービスの production の plan だけが出る。他サービス・他環境の plan は出ない +- [ ] release PR を merge すると、承認待ちなしで、そのサービスの production だけが apply される +- [ ] tag を指定した手動実行で、同じ apply を再実行できる +- [ ] `master` だけのサービス(`route53`・`cost-management`)と `github/` は、今までどおり動く diff --git a/scripts/terragrunt-release/check-release-configs.sh b/scripts/terragrunt-release/check-release-configs.sh new file mode 100755 index 00000000..0d669b6e --- /dev/null +++ b/scripts/terragrunt-release/check-release-configs.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +config_path="${1:-workflow-config.yaml}" + +# release-targets.sh stderr is hidden below, so an unreadable config would otherwise look like "no release services" +yq '.' "${config_path}" >/dev/null + +merge_environments="$(yq -o=json '.' "${config_path}" | jq -c ' + [.stacks[] | select(.name == "terragrunt") | .environments | to_entries[] | select(.value.apply_trigger == "merge") | .key] +')" + +failed=0 + +report() { + echo "::error::$*" >&2 + failed=1 +} + +shopt -s nullglob +for service_dir in aws/*/; do + service="$(basename "${service_dir}")" + package="aws/${service}" + config_file="${package}/release-please-config.json" + manifest_file="${package}/release-please-manifest.json" + + if ! targets="$(bash "${script_dir}/release-targets.sh" "${service}" "${config_path}" 2>/dev/null)"; then + if [ -f "${config_file}" ]; then + report "${config_file} exists but ${service} has no release environment" + fi + continue + fi + + incomplete="$(jq -r ' + .[] | . as $target + | ("aws_region", "iam_role_plan", "iam_role_apply") as $field + | select(($target[$field] // "") == "") + | "::error::release target \($target.service)/\($target.environment) has no \($field)" + ' <<<"${targets}")" + if [ -n "${incomplete}" ]; then + echo "${incomplete}" >&2 + failed=1 + fi + + if [ ! -f "${config_file}" ]; then + report "${config_file} is missing but ${service} has a release environment" + continue + fi + + component="$(jq -r --arg package "${package}" '.packages[$package].component // ""' "${config_file}")" + if [ "${component}" != "${service}" ]; then + report "${package}: component must be ${service} (got '${component}')" + fi + + expected_exclude_paths="$(jq -c --arg package "${package}" 'map("\($package)/\(.)")' <<<"${merge_environments}")" + actual_exclude_paths="$(jq -c --arg package "${package}" '.packages[$package]["exclude-paths"]' "${config_file}")" + if ! jq -ne --argjson expected "${expected_exclude_paths}" --argjson actual "${actual_exclude_paths}" ' + ($actual | type) == "array" and ($actual | sort) == ($expected | sort) + ' >/dev/null; then + report "${package}: exclude-paths must be ${expected_exclude_paths} (got ${actual_exclude_paths})" + fi + + if [ ! -f "${manifest_file}" ] || ! jq -e --arg package "${package}" 'has($package)' "${manifest_file}" >/dev/null; then + report "${manifest_file} must have the key ${package}" + fi +done + +exit "${failed}" diff --git a/scripts/terragrunt-release/release-service.sh b/scripts/terragrunt-release/release-service.sh new file mode 100755 index 00000000..27ebbb99 --- /dev/null +++ b/scripts/terragrunt-release/release-service.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -euo pipefail + +mode="${1:-}" +value="${2:-}" + +case "${mode}" in + --tag) + pattern='^([a-z0-9-]+)-v[0-9]+\.[0-9]+\.[0-9]+$' + expected='-vX.Y.Z' + ;; + --branch) + pattern='^release-please--branches--main--components--([a-z0-9-]+)$' + expected='release-please--branches--main--components--' + ;; + *) + echo "::error::usage: release-service.sh --tag | --branch " >&2 + exit 1 + ;; +esac + +if [[ ! "${value}" =~ ${pattern} ]]; then + shown="${value//$'\n'/ }" + shown="${shown//$'\r'/ }" + echo "::error::'${shown}' does not match ${expected}" >&2 + exit 1 +fi + +echo "${BASH_REMATCH[1]}" diff --git a/scripts/terragrunt-release/release-targets.sh b/scripts/terragrunt-release/release-targets.sh new file mode 100755 index 00000000..8cc1aa50 --- /dev/null +++ b/scripts/terragrunt-release/release-targets.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +set -euo pipefail + +service="${1:?usage: release-targets.sh [config_path]}" +config_path="${2:-workflow-config.yaml}" + +candidates="$(yq -o=json '.' "${config_path}" | jq -c --arg service "${service}" ' + .stacks[] + | select(.name == "terragrunt") + | (.exclude // []) as $exclude + | .environments | to_entries[] + | select(.value.apply_trigger == "release") + | {service: $service, environment: .key} as $values + | { + service: $service, + environment: .key, + working_directory: "aws/\($service)/\(.key)", + aws_region: .value.aws_region, + iam_role_plan: .value.iam_role_plan, + iam_role_apply: .value.iam_role_apply, + excluded: any($exclude[]; to_entries | all(.[]; . as $pair | $values | has($pair.key) and .[$pair.key] == $pair.value)) + } +')" + +targets="$( + while IFS= read -r candidate; do + [ -n "${candidate}" ] || continue + if [ -d "$(jq -r '.working_directory' <<<"${candidate}")" ]; then + echo "${candidate}" + fi + done <<<"${candidates}" | jq -c -s '.' +)" + +if [ "$(jq 'length' <<<"${targets}")" -eq 0 ]; then + echo "::error::no release targets for ${service}" >&2 + exit 1 +fi + +jq -c 'map(select(.excluded | not) | del(.excluded))' <<<"${targets}" diff --git a/scripts/terragrunt-release/test.sh b/scripts/terragrunt-release/test.sh new file mode 100755 index 00000000..9c18da7a --- /dev/null +++ b/scripts/terragrunt-release/test.sh @@ -0,0 +1,374 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(git -C "${script_dir}" rev-parse --show-toplevel)" +work_root="$(mktemp -d)" +trap 'rm -rf "${work_root}"' EXIT + +last_stderr="" + +assert_eq() { + local expected="$1" actual="$2" + if [ "${expected}" != "${actual}" ]; then + echo " expected: ${expected}" >&2 + echo " actual: ${actual}" >&2 + exit 1 + fi +} + +assert_fails() { + local stderr_file="${work_root}/stderr" + if "$@" 2>"${stderr_file}"; then + echo " expected a non-zero exit: $*" >&2 + exit 1 + fi + last_stderr="$(cat "${stderr_file}")" +} + +assert_stderr_contains() { + local needle="$1" + if [[ "${last_stderr}" != *"${needle}"* ]]; then + echo " stderr does not contain: ${needle}" >&2 + echo " stderr: ${last_stderr}" >&2 + exit 1 + fi +} + +test_validate_accepts_merge_and_release() { + cat > workflow-config.yaml <<'YAML' +stacks: + - name: terragrunt + environments: + master: + apply_trigger: merge + production: + apply_trigger: release +YAML + bash "${script_dir}/validate-apply-trigger.sh" +} + +test_validate_rejects_missing_trigger() { + cat > workflow-config.yaml <<'YAML' +stacks: + - name: terragrunt + environments: + master: + apply_trigger: merge + production: + aws_region: ap-northeast-1 +YAML + assert_fails bash "${script_dir}/validate-apply-trigger.sh" + assert_stderr_contains "environments.production.apply_trigger" +} + +test_validate_rejects_unknown_value() { + cat > workflow-config.yaml <<'YAML' +stacks: + - name: terragrunt + environments: + production: + apply_trigger: relase +YAML + assert_fails bash "${script_dir}/validate-apply-trigger.sh" + assert_stderr_contains "(got 'relase')" +} + +test_validate_ignores_non_terragrunt_stacks() { + cat > workflow-config.yaml <<'YAML' +stacks: + - name: terragrunt + environments: + master: + apply_trigger: merge + - name: kubernetes + paths: + - "kubernetes/components/{service}/{environment}" +YAML + bash "${script_dir}/validate-apply-trigger.sh" +} + +test_repository_config_is_valid() { + bash "${script_dir}/validate-apply-trigger.sh" "${repo_root}/workflow-config.yaml" +} + +test_repository_config_never_merges_production() { + local trigger + trigger="$(yq '.stacks[] | select(.name == "terragrunt") | .environments.production.apply_trigger' "${repo_root}/workflow-config.yaml")" + assert_eq "release" "${trigger}" +} + +test_release_service_parses_tag() { + assert_eq "eks-holmesgpt" "$(bash "${script_dir}/release-service.sh" --tag eks-holmesgpt-v1.2.3)" +} + +test_release_service_rejects_root_tag() { + assert_fails bash "${script_dir}/release-service.sh" --tag v0.1.0 + assert_stderr_contains "::error::" +} + +test_release_service_rejects_flux_tag() { + assert_fails bash "${script_dir}/release-service.sh" --tag kubernetes-production + assert_stderr_contains "::error::" +} + +test_release_service_parses_branch() { + assert_eq "vpc" "$(bash "${script_dir}/release-service.sh" --branch release-please--branches--main--components--vpc)" +} + +test_release_service_rejects_root_branch() { + assert_fails bash "${script_dir}/release-service.sh" --branch release-please--branches--main + assert_stderr_contains "::error::" +} + +test_release_service_error_is_single_line() { + assert_fails bash "${script_dir}/release-service.sh" --tag $'vpc-v1.2.3\n::warning::x' + assert_stderr_contains "::error::" + assert_eq "1" "$(printf '%s\n' "${last_stderr}" | wc -l | tr -d ' ')" +} + +write_release_config() { + local exclude="${1:-}" + cat > workflow-config.yaml <<'YAML' +stacks: + - name: terragrunt-shared + attributes: {} + - name: terragrunt + environments: + master: + aws_region: ap-northeast-1 + iam_role_plan: arn:aws:iam::111111111111:role/master-plan + iam_role_apply: arn:aws:iam::111111111111:role/master-apply + apply_trigger: merge + production: + aws_region: ap-northeast-1 + iam_role_plan: arn:aws:iam::222222222222:role/production-plan + iam_role_apply: arn:aws:iam::222222222222:role/production-apply + apply_trigger: release + staging: + aws_region: us-east-1 + iam_role_plan: arn:aws:iam::333333333333:role/staging-plan + iam_role_apply: arn:aws:iam::333333333333:role/staging-apply + apply_trigger: release + - name: kubernetes + paths: + - "kubernetes/components/{service}/{environment}" +YAML + if [ -n "${exclude}" ]; then + EXCLUDE="${exclude}" yq -i '(.stacks[] | select(.name == "terragrunt") | .exclude) = env(EXCLUDE)' workflow-config.yaml + fi +} + +test_release_targets_lists_release_envs() { + write_release_config + mkdir -p aws/vpc/master aws/vpc/production + local expected='[{"service":"vpc","environment":"production","working_directory":"aws/vpc/production","aws_region":"ap-northeast-1","iam_role_plan":"arn:aws:iam::222222222222:role/production-plan","iam_role_apply":"arn:aws:iam::222222222222:role/production-apply"}]' + assert_eq "${expected}" "$(bash "${script_dir}/release-targets.sh" vpc)" +} + +test_release_targets_skips_missing_dirs() { + write_release_config + mkdir -p aws/vpc/production + local targets + targets="$(bash "${script_dir}/release-targets.sh" vpc)" + assert_eq "1" "$(jq 'length' <<<"${targets}")" + assert_eq "production" "$(jq -r '.[0].environment' <<<"${targets}")" +} + +test_release_targets_fails_without_targets() { + write_release_config + mkdir -p aws/route53/master + assert_fails bash "${script_dir}/release-targets.sh" route53 + assert_stderr_contains "no release targets for route53" +} + +test_release_targets_skips_excluded_envs() { + write_release_config '[{environment: production}]' + mkdir -p aws/vpc/production aws/vpc/staging + local targets + targets="$(bash "${script_dir}/release-targets.sh" vpc)" + assert_eq "1" "$(jq 'length' <<<"${targets}")" + assert_eq "staging" "$(jq -r '.[0].environment' <<<"${targets}")" +} + +test_release_targets_returns_empty_when_all_excluded() { + write_release_config '[{environment: production}, {environment: staging}]' + mkdir -p aws/vpc/production + assert_eq "[]" "$(bash "${script_dir}/release-targets.sh" vpc)" +} + +test_release_targets_honors_service_scoped_exclude() { + write_release_config '[{service: vpc, environment: production}]' + mkdir -p aws/vpc/production aws/eks/production + assert_eq "[]" "$(bash "${script_dir}/release-targets.sh" vpc)" + local targets + targets="$(bash "${script_dir}/release-targets.sh" eks)" + assert_eq "1" "$(jq 'length' <<<"${targets}")" + assert_eq "production" "$(jq -r '.[0].environment' <<<"${targets}")" +} + +test_release_targets_fails_without_dirs_even_with_exclude() { + write_release_config '[{environment: production}]' + mkdir -p aws/route53/master + assert_fails bash "${script_dir}/release-targets.sh" route53 + assert_stderr_contains "no release targets for route53" +} + +write_service_release_config() { + local service="$1" component="${2:-$1}" exclude_paths="${3:-[\"aws/$1/master\"]}" + mkdir -p "aws/${service}" + cat > "aws/${service}/release-please-config.json" < "aws/${service}/release-please-manifest.json" +} + +test_check_configs_accepts_matching_set() { + write_release_config + mkdir -p aws/vpc/production aws/route53/master + write_service_release_config vpc + assert_eq "" "$(bash "${script_dir}/check-release-configs.sh" 2>&1)" +} + +test_check_configs_rejects_missing_config() { + write_release_config + mkdir -p aws/eks/production + assert_fails bash "${script_dir}/check-release-configs.sh" + assert_stderr_contains "aws/eks/release-please-config.json" +} + +test_check_configs_requires_config_for_excluded_env() { + write_release_config '[{environment: production}]' + mkdir -p aws/eks/production + assert_fails bash "${script_dir}/check-release-configs.sh" + assert_stderr_contains "aws/eks/release-please-config.json" + write_service_release_config eks + assert_eq "" "$(bash "${script_dir}/check-release-configs.sh" 2>&1)" +} + +test_check_configs_rejects_orphan_config() { + write_release_config + mkdir -p aws/route53/master + write_service_release_config route53 + assert_fails bash "${script_dir}/check-release-configs.sh" + assert_stderr_contains "aws/route53/release-please-config.json" +} + +test_check_configs_rejects_wrong_component() { + write_release_config + mkdir -p aws/vpc/production + write_service_release_config vpc eks + assert_fails bash "${script_dir}/check-release-configs.sh" + assert_stderr_contains "aws/vpc: component must be vpc (got 'eks')" +} + +test_check_configs_rejects_manifest_without_package() { + write_release_config + mkdir -p aws/vpc/production + write_service_release_config vpc + echo '{}' > aws/vpc/release-please-manifest.json + assert_fails bash "${script_dir}/check-release-configs.sh" + assert_stderr_contains "aws/vpc/release-please-manifest.json" + rm aws/vpc/release-please-manifest.json + assert_fails bash "${script_dir}/check-release-configs.sh" + assert_stderr_contains "aws/vpc/release-please-manifest.json" +} + +test_check_configs_rejects_incomplete_release_env() { + cat > workflow-config.yaml <<'YAML' +stacks: + - name: terragrunt + environments: + production: + apply_trigger: release +YAML + mkdir -p aws/vpc/production + write_service_release_config vpc + assert_fails bash "${script_dir}/check-release-configs.sh" + assert_stderr_contains "vpc/production" + assert_stderr_contains "aws_region" + assert_stderr_contains "iam_role_plan" + assert_stderr_contains "iam_role_apply" +} + +test_check_configs_rejects_missing_exclude_paths() { + write_release_config + mkdir -p aws/vpc/production + write_service_release_config vpc + jq -c 'del(.packages["aws/vpc"]["exclude-paths"])' aws/vpc/release-please-config.json > config.tmp + mv config.tmp aws/vpc/release-please-config.json + assert_fails bash "${script_dir}/check-release-configs.sh" + assert_stderr_contains 'aws/vpc: exclude-paths must be ["aws/vpc/master"] (got null)' +} + +test_check_configs_rejects_relative_exclude_paths() { + write_release_config + mkdir -p aws/vpc/production + write_service_release_config vpc vpc '["master"]' + assert_fails bash "${script_dir}/check-release-configs.sh" + assert_stderr_contains 'aws/vpc: exclude-paths must be ["aws/vpc/master"]' +} + +test_check_configs_accepts_exclude_paths_in_any_order() { + cat > workflow-config.yaml <<'YAML' +stacks: + - name: terragrunt + environments: + master: + apply_trigger: merge + develop: + apply_trigger: merge + production: + aws_region: ap-northeast-1 + iam_role_plan: arn:aws:iam::222222222222:role/production-plan + iam_role_apply: arn:aws:iam::222222222222:role/production-apply + apply_trigger: release +YAML + mkdir -p aws/vpc/production + write_service_release_config vpc vpc '["aws/vpc/develop","aws/vpc/master"]' + assert_eq "" "$(bash "${script_dir}/check-release-configs.sh" 2>&1)" +} + +test_deploy_trigger_applies_only_merge_targets() { + local expected="\${{ matrix.target.stack == 'terragrunt' && (github.event_name == 'pull_request' && 'plan' || (matrix.target.apply_trigger == 'merge' && 'apply' || '')) || '' }}" + local actual + actual="$(yq '.jobs.deploy-terragrunt.with.action-type' "${repo_root}/.github/workflows/auto-label--deploy-trigger.yaml")" + assert_eq "${expected}" "${actual}" +} + +test_deploy_trigger_excludes_shared_targets() { + local job stack + for job in deploy-terragrunt deploy-kubernetes; do + stack="$(yq ".jobs.${job}.strategy.matrix.exclude[] | select(.target.stack == \"terragrunt-shared\") | .target.stack" "${repo_root}/.github/workflows/auto-label--deploy-trigger.yaml")" + assert_eq "terragrunt-shared" "${stack}" + done +} + +test_release_workflows_skip_empty_targets() { + local expected="needs.resolve.outputs.targets != '[]'" + assert_eq "${expected}" "$(yq '.jobs.plan.if' "${repo_root}/.github/workflows/auto-release--plan.yaml")" + assert_eq "${expected}" "$(yq '.jobs.apply.if' "${repo_root}/.github/workflows/auto-release--trigger.yaml")" +} + +test_repository_config_excludes_production() { + local excluded + excluded="$(yq '.stacks[] | select(.name == "terragrunt") | .exclude[] | select(.environment == "production") | .environment' "${repo_root}/workflow-config.yaml")" + assert_eq "production" "${excluded}" +} + +failed=0 +for test_name in $(compgen -A function test_); do + fixture="$(mktemp -d "${work_root}/fixture.XXXXXX")" + set +e + (set -e; cd "${fixture}"; "${test_name}") + status=$? + set -e + if [ "${status}" -eq 0 ]; then + echo "PASS: ${test_name}" + else + echo "FAIL: ${test_name}" + failed=1 + fi +done + +exit "${failed}" diff --git a/scripts/terragrunt-release/validate-apply-trigger.sh b/scripts/terragrunt-release/validate-apply-trigger.sh new file mode 100755 index 00000000..dacd5a44 --- /dev/null +++ b/scripts/terragrunt-release/validate-apply-trigger.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -euo pipefail + +config_path="${1:-workflow-config.yaml}" + +errors="$(yq -o=json '.' "${config_path}" | jq -r --arg q "'" ' + .stacks | to_entries[] + | select(.value.name == "terragrunt") + | .key as $index + | .value.environments | to_entries[] + | select(.value.apply_trigger | IN("merge", "release") | not) + | "::error::stacks[\($index)].environments.\(.key).apply_trigger must be merge or release (got \($q)\(.value.apply_trigger)\($q))" +')" + +if [ -n "${errors}" ]; then + echo "${errors}" >&2 + exit 1 +fi diff --git a/workflow-config.yaml b/workflow-config.yaml index efc043f8..f81b49b0 100644 --- a/workflow-config.yaml +++ b/workflow-config.yaml @@ -8,6 +8,24 @@ stacks: aws_region: ap-northeast-1 iam_role_plan: arn:aws:iam::559744160976:role/github-oidc-auth-master-github-actions-plan-role iam_role_apply: arn:aws:iam::559744160976:role/github-oidc-auth-master-github-actions-apply-role + apply_trigger: merge + develop: + aws_region: ap-northeast-1 + iam_role_plan: arn:aws:iam::270242382571:role/github-oidc-auth-develop-github-actions-plan-role + iam_role_apply: arn:aws:iam::270242382571:role/github-oidc-auth-develop-github-actions-apply-role + apply_trigger: merge + production: + aws_region: ap-northeast-1 + iam_role_plan: arn:aws:iam::337169763788:role/github-oidc-auth-production-github-actions-plan-role + iam_role_apply: arn:aws:iam::337169763788:role/github-oidc-auth-production-github-actions-apply-role + apply_trigger: release + exclude: + - environment: production + + - name: terragrunt-shared + paths: + - "aws/{service}" + attributes: {} - name: kubernetes paths: