From 0916153a402ed8ac8a5fee7cee46739af76f6fbb Mon Sep 17 00:00:00 2001 From: Brend Smits Date: Thu, 11 Jun 2026 14:14:34 +0200 Subject: [PATCH 1/3] fix(runners): fix type mismatch in pool role conditional The conditional for the runner role in pool.tf returned inconsistent types: the true branch produced an object with a single 'arn' attribute while the false branch returned the full aws_iam_role resource (16 attributes). Terraform requires both branches to have consistent types. Move the conditional inside the object so both branches produce a string for the 'arn' key. Fixes type error introduced in #4875. Signed-off-by: Brend Smits --- modules/runners/pool.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/runners/pool.tf b/modules/runners/pool.tf index 16df2d15d1..ae82f3f127 100644 --- a/modules/runners/pool.tf +++ b/modules/runners/pool.tf @@ -51,7 +51,7 @@ module "pool" { group_name = var.runner_group_name name_prefix = var.runner_name_prefix pool_owner = var.pool_runner_owner - role = var.iam_overrides["override_runner_role"] ? { arn = var.iam_overrides["runner_role_arn"] } : aws_iam_role.runner[0] + role = { arn = var.iam_overrides["override_runner_role"] ? var.iam_overrides["runner_role_arn"] : aws_iam_role.runner[0].arn } use_dedicated_host = var.use_dedicated_host } subnet_ids = var.subnet_ids From 478c28f302f491993e09c55426407a544dae9660 Mon Sep 17 00:00:00 2001 From: Brend Smits Date: Thu, 11 Jun 2026 14:29:12 +0200 Subject: [PATCH 2/3] ci(runners): add terraform test for pool module type safety Add a terraform test that exercises the pool module code path with mock providers. This catches conditional type mismatches (like the one in #4875) that terraform validate cannot detect. The test uses mock_provider with mock_data to avoid needing AWS credentials while still running a full plan that validates type consistency. Also adds a terraform_test job to the existing CI workflow that runs `terraform test` on modules with test files. Signed-off-by: Brend Smits --- .github/workflows/terraform.yml | 25 ++++++++++++ modules/runners/tests/pool.tftest.hcl | 56 +++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 modules/runners/tests/pool.tftest.hcl diff --git a/.github/workflows/terraform.yml b/.github/workflows/terraform.yml index 872943e659..1ff51af7ee 100644 --- a/.github/workflows/terraform.yml +++ b/.github/workflows/terraform.yml @@ -206,3 +206,28 @@ jobs: run: | tflint --init -c ${GITHUB_WORKSPACE}/.tflint.hcl --chdir "examples/${EXAMPLE_NAME}" tflint -f compact -c ${GITHUB_WORKSPACE}/.tflint.hcl --var-file ${GITHUB_WORKSPACE}/.github/lint/tflint.tfvars --chdir "examples/${EXAMPLE_NAME}" + + terraform_test: + name: Terraform test + strategy: + fail-fast: false + matrix: + module: + - modules/runners + defaults: + run: + working-directory: ${{ matrix.module }} + runs-on: ubuntu-latest + container: + image: hashicorp/terraform:latest + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0 + with: + egress-policy: audit + + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + - name: terraform test + run: terraform test -test-directory=tests diff --git a/modules/runners/tests/pool.tftest.hcl b/modules/runners/tests/pool.tftest.hcl new file mode 100644 index 0000000000..5606fdd5a5 --- /dev/null +++ b/modules/runners/tests/pool.tftest.hcl @@ -0,0 +1,56 @@ +mock_provider "aws" { + mock_data "aws_iam_policy_document" { + defaults = { + json = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"lambda.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}" + } + } +} + +variables { + aws_region = "eu-west-1" + vpc_id = "vpc-12345678" + subnet_ids = ["subnet-12345678"] + + instance_types = ["m5.large"] + + s3_runner_binaries = { + arn = "arn:aws:s3:::my-bucket" + id = "my-bucket" + key = "runners/linux/actions-runner.tar.gz" + } + + sqs_build_queue = { + arn = "arn:aws:sqs:eu-west-1:123456789012:build-queue" + url = "https://sqs.eu-west-1.amazonaws.com/123456789012/build-queue" + } + + enable_organization_runners = true + enable_ssm_on_runners = true + runner_labels = ["self-hosted", "linux", "x64"] + + github_app_parameters = { + key_base64 = { name = "/github-runner/key-base64", arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/key-base64" } + id = { name = "/github-runner/app-id", arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/app-id" } + } + + ssm_paths = { + root = "/github-runner" + tokens = "tokens" + config = "config" + } + + # Enable pool to exercise the pool module and its role type + pool_config = [{ + schedule_expression = "cron(0 8 * * ? *)" + size = 1 + }] +} + +run "plan_with_pool_enabled" { + command = plan + + assert { + condition = length(module.pool) == 1 + error_message = "Pool module should be enabled when pool_config is non-empty" + } +} From 7df0fb9bfaae5b49a64916c643964e9485d3c4ec Mon Sep 17 00:00:00 2001 From: Brend Smits Date: Thu, 11 Jun 2026 14:33:19 +0200 Subject: [PATCH 3/3] docs(runners): add terraform test README and pin CI image - Add README.md explaining how to write and run terraform tests - Pin CI container to hashicorp/terraform:1.12 (mock_provider needs >= 1.7) Signed-off-by: Brend Smits --- .github/workflows/terraform.yml | 4 +- modules/runners/tests/README.md | 72 +++++++++++++++++++++++++++ modules/runners/tests/pool.tftest.hcl | 4 ++ 3 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 modules/runners/tests/README.md diff --git a/.github/workflows/terraform.yml b/.github/workflows/terraform.yml index 1ff51af7ee..093a728b8c 100644 --- a/.github/workflows/terraform.yml +++ b/.github/workflows/terraform.yml @@ -219,7 +219,7 @@ jobs: working-directory: ${{ matrix.module }} runs-on: ubuntu-latest container: - image: hashicorp/terraform:latest + image: hashicorp/terraform@sha256:1d10ec4073f4ddbdf34a28540a3b9250852ab500cb1c53f68c8bd17d82f474d8 # 1.14 steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0 @@ -229,5 +229,7 @@ jobs: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false + - name: terraform init + run: terraform init -backend=false -input=false - name: terraform test run: terraform test -test-directory=tests diff --git a/modules/runners/tests/README.md b/modules/runners/tests/README.md new file mode 100644 index 0000000000..fa55dfecd9 --- /dev/null +++ b/modules/runners/tests/README.md @@ -0,0 +1,72 @@ +# Terraform Tests + +This directory contains [Terraform test files](https://developer.hashicorp.com/terraform/language/tests) (`.tftest.hcl`) for the runners module. + +## Why `terraform test` instead of `terraform validate`? + +`terraform validate` only checks syntax and basic type correctness of the configuration. It **cannot** detect: + +- Conditional expressions with inconsistent result types (e.g., one branch returns an object with 1 attribute, the other returns 16) +- Runtime type mismatches that only surface during `plan` +- Invalid cross-module references that depend on resource attribute shapes + +`terraform test` with `mock_provider` runs a full plan without needing real cloud credentials, catching these classes of bugs in CI. + +## Requirements + +- Terraform >= 1.7 (for `mock_provider` and `mock_data` support) +- No AWS credentials required — all providers are mocked + +## Running locally + +```bash +cd modules/runners +terraform test -test-directory=tests +``` + +Expected output: + +``` +tests/pool.tftest.hcl... in progress + run "plan_with_pool_enabled"... pass +tests/pool.tftest.hcl... pass + +Success! 1 passed, 0 failed. +``` + +## Writing new tests + +1. Create a `.tftest.hcl` file in this directory +2. Use `mock_provider "aws" {}` to avoid needing credentials +3. Use `mock_data` blocks to provide realistic values for data sources that perform validation (e.g., `aws_iam_policy_document` validates JSON) +4. Set all required variables in a `variables {}` block +5. Use `run` blocks with `command = plan` and `assert` conditions + +### Example template + +```hcl +mock_provider "aws" { + mock_data "aws_iam_policy_document" { + defaults = { + json = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"lambda.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}" + } + } +} + +variables { + # ... required variables ... +} + +run "descriptive_test_name" { + command = plan + + assert { + condition = + error_message = "Explanation of what failed" + } +} +``` + +## CI integration + +These tests run automatically in the `terraform_test` job of `.github/workflows/terraform.yml` on every PR that touches `*.tf` or `*.hcl` files. diff --git a/modules/runners/tests/pool.tftest.hcl b/modules/runners/tests/pool.tftest.hcl index 5606fdd5a5..2c557f9392 100644 --- a/modules/runners/tests/pool.tftest.hcl +++ b/modules/runners/tests/pool.tftest.hcl @@ -28,6 +28,10 @@ variables { enable_ssm_on_runners = true runner_labels = ["self-hosted", "linux", "x64"] + # Use S3 bucket to avoid filebase64sha256 needing local zip files + lambda_s3_bucket = "my-lambda-bucket" + runners_lambda_s3_key = "runners.zip" + github_app_parameters = { key_base64 = { name = "/github-runner/key-base64", arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/key-base64" } id = { name = "/github-runner/app-id", arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/app-id" }