diff --git a/.github/workflows/terraform-integration.yml b/.github/workflows/terraform-integration.yml index 83811e24e4..1664542371 100644 --- a/.github/workflows/terraform-integration.yml +++ b/.github/workflows/terraform-integration.yml @@ -19,7 +19,7 @@ env: TERRAFORM_AWS_MODULES_DIR: modules/terraform/aws jobs: - terraform-test: + terraform-integration: environment: terraform-privileged permissions: contents: read @@ -59,119 +59,117 @@ jobs: env: ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - setup-matrix: - runs-on: ubuntu-latest - name: Setup terraform plan matrix for test scenarios - steps: - - name: Checkout PR head - uses: actions/checkout@v4 - with: - repository: ${{ github.event.pull_request.head.repo.full_name }} - ref: ${{ github.event.pull_request.head.sha }} - fetch-depth: 0 - persist-credentials: false - - - name: Setup test matrix scenarios - id: setup-matrix-scenarios + - name: Compute scenarios to plan + id: scenarios + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + REPO: ${{ github.repository }} + FULL_MATRIX_QUERY: | + find "$GITHUB_WORKSPACE/scenarios/" -name '*.tfvars' | grep 'azure' | awk -F'/' '{ + split($11, file_name, "."); + split(file_name[1], cloud_region, "-"); + region = (length(cloud_region) > 1) ? substr($11, index($11, "-") + 1) : ""; + cloud = cloud_region[1]; + gsub(".json", "", region); + printf "{\"cloud\": \"%s\", \"file_name\": \"%s\", \"scenario_type\": \"%s\", \"scenario_name\": \"%s\"}\n", cloud, file_name[1], $8, $9; + }' | sort -u | jq -s 'unique' run: | - set -eux - matrix=$(find $GITHUB_WORKSPACE/scenarios/ -name "*.tfvars" | grep 'azure' | awk -F'/' '{split($11, file_name, "."); split(file_name[1], cloud_region, "-");region= (length(cloud_region) > 1) ? substr($11, index($11, "-") + 1) : ""; cloud=cloud_region[1]; gsub(".json", "", region); print "{\"cloud\": \"" cloud "\", \"file_name\": \"" file_name[1] "\", " (region != "" ? "\"region\": \"" region "\", " : "") "\"scenario_type\": \"" $8 "\", \"scenario_name\": \"" $9 "\"},"}' | sort | uniq | sed 's/,$/,/') - matrix_array="[${matrix%,}]" + set -eu + matrix_array=$(bash -c "$FULL_MATRIX_QUERY") - file_changes=$(git diff --name-only -r origin/main HEAD) - echo "PR file changes: $file_changes" + # Ask the GitHub API for the PR's changed files. This avoids depending + # on git remote refs (fork PRs don't have origin/main pointing at the + # base branch), and doesn't require fetch-depth: 0. + file_changes=$(gh api --paginate "/repos/$REPO/pulls/$PR_NUMBER/files" --jq '.[].filename') + echo "PR file changes:" + echo "$file_changes" if [ -z "$file_changes" ]; then - matrix_combinations=null - echo "matrix_combinations=$matrix_combinations" >> "$GITHUB_OUTPUT" - else - cloud_changes=$(echo "$file_changes" | grep -E '\.tf$' | awk -F'/' '{print $3}' | uniq) - echo "File changes related to cloud: $cloud_changes" - - run_all_tests=false - module_matrix='[]' - scenario_matrix='[]' - if [ $(echo "$cloud_changes" | wc -w) -eq 1 ]; then - cloud=$(echo "$cloud_changes" | cut -d' ' -f1) - module_matrix=$(echo "$matrix_array" | jq --arg cloud_value "$cloud" '. | map(select(.cloud == $cloud_value))') - elif [ $(echo "$cloud_changes" | wc -w) -eq 0 ]; then - run_all_tests=false - else - run_all_tests=true - module_matrix=$(echo "$matrix_array") - fi - echo "module_matrix: $module_matrix" - if [ "$run_all_tests" = false ]; then - changed_scenario_names=$(echo "$file_changes" | grep -E '^scenarios/' | awk -F'/' '{print $3}' | tr ' ' '\n' | sort -u) - echo "Test scenarios changed: $changed_scenario_names" - for scenario_name in $changed_scenario_names; do - filtered_objects=$(echo "$matrix_array" | jq --arg scenario_value "$scenario_name" '. | map(select(.scenario_name == $scenario_value))') - scenario_matrix=$(echo "$scenario_matrix $filtered_objects" | jq -s 'flatten | unique') - done - fi - updated_matrix=$(echo "$module_matrix $scenario_matrix" | jq -s 'flatten | unique') - echo "Test scenarios to run: $updated_matrix" - updated_matrix="${updated_matrix//$'\n'/''}" - matrix_combinations="{\"include\": ${updated_matrix}}" - echo "matrix_combinations=$matrix_combinations" >> "$GITHUB_OUTPUT" + echo "scenarios=[]" >> "$GITHUB_OUTPUT" + echo "No file changes -> skipping plans." + exit 0 fi - echo "matrix_combinations: $matrix_combinations" - outputs: - matrix-combinations: ${{ steps.setup-matrix-scenarios.outputs.matrix_combinations }} - terraform-plan: - needs: setup-matrix - environment: terraform-privileged - permissions: - id-token: write - contents: read - if: ${{ needs.setup-matrix.result == 'success' && needs.setup-matrix.outputs.matrix-combinations != '' && needs.setup-matrix.outputs.matrix-combinations != 'null' }} - strategy: - fail-fast: false - matrix: ${{ fromJson(needs.setup-matrix.outputs.matrix-combinations) }} - runs-on: ubuntu-latest - name: ${{ matrix.cloud }}-${{ matrix.scenario_type }}-${{ matrix.scenario_name }} ${{ matrix.region }} + cloud_changes=$(echo "$file_changes" | grep -E '\.tf$' | awk -F'/' '{print $3}' | uniq || true) + echo "File changes related to cloud: $cloud_changes" - steps: - - name: Checkout PR head - uses: actions/checkout@v4 - with: - repository: ${{ github.event.pull_request.head.repo.full_name }} - ref: ${{ github.event.pull_request.head.sha }} - persist-credentials: false + run_all_tests=false + module_matrix='[]' + scenario_matrix='[]' + if [ "$(echo "$cloud_changes" | wc -w)" -eq 1 ]; then + cloud=$(echo "$cloud_changes" | cut -d' ' -f1) + module_matrix=$(echo "$matrix_array" | jq --arg c "$cloud" 'map(select(.cloud == $c))') + elif [ "$(echo "$cloud_changes" | wc -w)" -eq 0 ]; then + run_all_tests=false + else + run_all_tests=true + module_matrix=$matrix_array + fi - - name: Setup Terraform - uses: hashicorp/setup-terraform@v2 - with: - terraform_version: 1.10.0 + if [ "$run_all_tests" = false ]; then + changed_scenario_names=$(echo "$file_changes" | grep -E '^scenarios/' | awk -F'/' '{print $3}' | tr ' ' '\n' | sort -u || true) + for scenario_name in $changed_scenario_names; do + filtered=$(echo "$matrix_array" | jq --arg s "$scenario_name" 'map(select(.scenario_name == $s))') + scenario_matrix=$(echo "$scenario_matrix $filtered" | jq -s 'flatten | unique') + done + fi - - name: Get job id and set env - run: | - echo "TERRAFORM_INPUT_FILE=$GITHUB_WORKSPACE/scenarios/${{ matrix.scenario_type }}/${{ matrix.scenario_name }}/terraform-inputs/${{ matrix.file_name }}.tfvars" >> "$GITHUB_ENV" - echo "TERRAFORM_TEST_INPUT_FILE=$GITHUB_WORKSPACE/scenarios/${{ matrix.scenario_type }}/${{ matrix.scenario_name }}/terraform-test-inputs/${{ matrix.file_name }}.json" >> "$GITHUB_ENV" - echo "TERRAFORM_MODULES_DIR=modules/terraform/${{ matrix.cloud }}" >> "$GITHUB_ENV" + final=$(echo "$module_matrix $scenario_matrix" | jq -s 'flatten | unique') + echo "Scenarios to plan: $final" + echo "scenarios=$(echo "$final" | jq -c '.')" >> "$GITHUB_OUTPUT" - - name: Create JSON Input + - name: Terraform Plan for every scenario + if: steps.scenarios.outputs.scenarios != '[]' + env: + ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + SCENARIOS_JSON: ${{ steps.scenarios.outputs.scenarios }} run: | - INPUT_JSON_STRING=$(jq -c '. + {creation_time: (now | todateiso8601)}' "${{ env.TERRAFORM_TEST_INPUT_FILE }}") - echo "INPUT_JSON=$INPUT_JSON_STRING" >> "$GITHUB_ENV" + set -eu + + # Collect failures instead of bailing at the first one, so a single + # bad scenario doesn't hide the rest. + FAILED="" + + # Iterate via process substitution so we can mutate FAILED without + # losing it to a subshell. + while IFS=$'\t' read -r cloud scenario_type scenario_name file_name; do + modules_dir="modules/terraform/$cloud" + tfvars="$GITHUB_WORKSPACE/scenarios/$scenario_type/$scenario_name/terraform-inputs/${file_name}.tfvars" + test_input="$GITHUB_WORKSPACE/scenarios/$scenario_type/$scenario_name/terraform-test-inputs/${file_name}.json" + + label="$cloud/$scenario_type/$scenario_name/$file_name" + echo "::group::plan $label" + + if [ ! -f "$tfvars" ]; then + echo "::error::missing tfvars: $tfvars" + FAILED="${FAILED}${label}\n" + echo "::endgroup::" + continue + fi + if [ ! -f "$test_input" ]; then + echo "::error::missing test input: $test_input" + FAILED="${FAILED}${label}\n" + echo "::endgroup::" + continue + fi - - name: Azure login - if: ${{ matrix.cloud == 'azure' }} - uses: azure/login@v2 - with: - client-id: ${{ secrets.AZURE_MI }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + INPUT_JSON=$(jq -c '. + {creation_time: (now | todateiso8601)}' "$test_input") - - name: Terraform Init - working-directory: ${{ env.TERRAFORM_MODULES_DIR }} - run: terraform init + if ! ( + cd "$modules_dir" + terraform init -input=false + terraform plan \ + -var-file "$tfvars" \ + -var="json_input=$INPUT_JSON" + ); then + FAILED="${FAILED}${label}\n" + fi + echo "::endgroup::" + done < <(echo "$SCENARIOS_JSON" | jq -r '.[] | [.cloud, .scenario_type, .scenario_name, .file_name] | @tsv') - - name: Terraform Plan - working-directory: ${{ env.TERRAFORM_MODULES_DIR }} - run: | - terraform plan -var-file "$TERRAFORM_INPUT_FILE" -var="json_input=$INPUT_JSON" - env: - ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - CLOUD: ${{ matrix.cloud }} + if [ -n "$FAILED" ]; then + echo "::error::Failed scenarios:" + printf ' %b' "$FAILED" + exit 1 + fi