From f4f1c1fa87e2b43cfa151c8d482d99131d17d8a5 Mon Sep 17 00:00:00 2001 From: Riccardo Piccoli Date: Wed, 15 Jul 2026 11:37:53 +0200 Subject: [PATCH 1/2] Add tofu plan and validate workflows for pull requests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split into two workflows: - validate.yaml: runs on pull_request (no secrets needed), does tofu init -backend=false + tofu validate for immediate feedback on syntax/config errors. - plan.yaml: runs on pull_request_target with a required "plan" environment gate, so a maintainer must approve before secrets (AWS creds, GitHub App token) are exposed to the PR's code. Posts the plan output as a PR comment. The environment gate mitigates the Pwn Request risk inherent in pull_request_target + checkout PR head: external data sources and provider binaries execute during plan, so untrusted code must not run without human review of the diff. Also: - Pin all third-party actions to immutable commit SHAs - Set persist-credentials: false on all checkout steps - Paginate listComments to handle PRs with 30+ comments - Extract CONFIG_APP_CLIENT_ID env var in apply.yaml - Remove stale Prow required status checks (ci/prow/temp, ci/prow/unit) from fulfillment-service, osac-operator, osac-aap Note: the GitHub App token is intentionally NOT scoped to a single repository — the Terraform GitHub provider manages all repos in the org, so both plan and apply need org-wide access. Assisted-by: Claude Code Signed-off-by: Riccardo Piccoli --- .github/workflows/apply.yaml | 11 ++- .github/workflows/plan.yaml | 122 ++++++++++++++++++++++++++++++ .github/workflows/pre-commit.yaml | 10 ++- .github/workflows/validate.yaml | 36 +++++++++ repositories.tf | 10 +-- 5 files changed, 176 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/plan.yaml create mode 100644 .github/workflows/validate.yaml diff --git a/.github/workflows/apply.yaml b/.github/workflows/apply.yaml index 3f883a6..1a710f1 100644 --- a/.github/workflows/apply.yaml +++ b/.github/workflows/apply.yaml @@ -30,19 +30,22 @@ jobs: env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + CONFIG_APP_CLIENT_ID: Iv23lipEOAvwk5QqNUie TF_CLI_ARGS: "-no-color" TF_IN_AUTOMATION: "true" steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + persist-credentials: false - name: Generate a token id: generate-token - uses: actions/create-github-app-token@v3 + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3 with: - client-id: Iv23lipEOAvwk5QqNUie + client-id: ${{ env.CONFIG_APP_CLIENT_ID }} private-key: ${{ secrets.CONFIG_APP_SECRET }} owner: ${{ github.repository_owner }} - name: Setup OpenTofu - uses: opentofu/setup-opentofu@v2 + uses: opentofu/setup-opentofu@a1320f892987e89d278cc92dc5adc984fb93aca4 # v2 with: tofu_wrapper: false - name: TF init diff --git a/.github/workflows/plan.yaml b/.github/workflows/plan.yaml new file mode 100644 index 0000000..d1c215d --- /dev/null +++ b/.github/workflows/plan.yaml @@ -0,0 +1,122 @@ +name: Plan + +on: + pull_request_target: + paths: + - "**/*.tf" + - "**/*.csv" + +permissions: + contents: read + pull-requests: write + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + plan: + runs-on: ubuntu-latest + environment: plan + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + CONFIG_APP_CLIENT_ID: Iv23lipEOAvwk5QqNUie + TF_CLI_ARGS: "-no-color" + TF_IN_AUTOMATION: "true" + steps: + - name: Checkout PR + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false + + - name: Generate a token + id: generate-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3 + with: + client-id: ${{ env.CONFIG_APP_CLIENT_ID }} + private-key: ${{ secrets.CONFIG_APP_SECRET }} + owner: ${{ github.repository_owner }} + + - name: Setup OpenTofu + uses: opentofu/setup-opentofu@a1320f892987e89d278cc92dc5adc984fb93aca4 # v2 + with: + tofu_wrapper: false + + - name: TF init + run: tofu init + + - name: TF Validate + run: tofu validate + + - name: TF Plan + id: plan + run: | + set +e + tofu plan -detailed-exitcode 2>&1 | tee plan_output.txt + exitcode=${PIPESTATUS[0]} + echo "exitcode=$exitcode" >> "$GITHUB_OUTPUT" + if [ "$exitcode" -eq 1 ]; then + exit 1 + fi + env: + GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }} + + - name: Comment plan on PR + if: always() && steps.plan.outcome != 'skipped' + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 + with: + script: | + const fs = require('fs'); + const raw = fs.readFileSync('plan_output.txt', 'utf8'); + const plan = raw.replace(/&/g, '&').replace(//g, '>'); + const exitcode = '${{ steps.plan.outputs.exitcode }}'; + + let summary; + if (exitcode === '0') { + summary = 'No changes. Infrastructure is up-to-date.'; + } else if (exitcode === '2') { + summary = 'Changes detected. Review the plan below.'; + } else { + summary = 'Plan failed. See details below.'; + } + + const marker = ''; + const body = [ + marker, + `### OpenTofu Plan`, + '', + summary, + '', + '
Show plan output', + '', + `
${plan.substring(0, 60000)}
`, + '', + '
', + '', + `*Commit: ${context.payload.pull_request.head.sha}*`, + ].join('\n'); + + const comments = await github.paginate(github.rest.issues.listComments, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + const existing = comments.find(c => c.body.includes(marker)); + + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); + } diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml index 1a2df67..307ce4c 100644 --- a/.github/workflows/pre-commit.yaml +++ b/.github/workflows/pre-commit.yaml @@ -13,13 +13,15 @@ jobs: env: TF_CLI_ARGS: "-no-color" steps: - - uses: actions/checkout@v7 - - uses: actions/setup-python@v6 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + persist-credentials: false + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: python-version: "3.13" - - uses: opentofu/setup-opentofu@v2 + - uses: opentofu/setup-opentofu@a1320f892987e89d278cc92dc5adc984fb93aca4 # v2 with: tofu_wrapper: false - - uses: pre-commit/action@v3.0.1 + - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 with: extra_args: --all-files diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml new file mode 100644 index 0000000..9276449 --- /dev/null +++ b/.github/workflows/validate.yaml @@ -0,0 +1,36 @@ +name: Validate + +on: + pull_request: + paths: + - "**/*.tf" + - "**/*.csv" + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + validate: + runs-on: ubuntu-latest + env: + TF_CLI_ARGS: "-no-color" + TF_IN_AUTOMATION: "true" + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + persist-credentials: false + + - name: Setup OpenTofu + uses: opentofu/setup-opentofu@a1320f892987e89d278cc92dc5adc984fb93aca4 # v2 + with: + tofu_wrapper: false + + - name: TF init (no backend) + run: tofu init -backend=false + + - name: TF Validate + run: tofu validate diff --git a/repositories.tf b/repositories.tf index 9fb3027..ba38719 100644 --- a/repositories.tf +++ b/repositories.tf @@ -36,7 +36,7 @@ module "repo_github_config" { use_public_template = false required_approvals = 2 required_status_checks = [ - { context = "pre-commit", integration_id = 15368 }, + "pre-commit", ] teams = [ @@ -107,7 +107,7 @@ module "repo_fulfillment_service" { ] required_approvals = null required_status_checks = [ - { context = "e2e-vmaas-full-install / e2e", integration_id = 15368 }, + "e2e-vmaas-full-install / e2e" ] push_allowances = ["/openshift-merge-robot", "osac-project/wg-infra", "osac-project/org-admins"] environments = [{ name = "e2e-test" }] @@ -137,7 +137,7 @@ module "repo_cloudkit_operator" { ] required_approvals = null required_status_checks = [ - { context = "e2e-vmaas-full-install / e2e", integration_id = 15368 }, + "e2e-vmaas-full-install / e2e" ] push_allowances = ["/openshift-merge-robot", "osac-project/wg-infra", "osac-project/org-admins"] environments = [{ name = "e2e-test" }] @@ -160,7 +160,7 @@ module "repo_cloudkit_aap" { ] required_approvals = null required_status_checks = [ - { context = "e2e-vmaas-full-install / e2e", integration_id = 15368 }, + "e2e-vmaas-full-install / e2e" ] push_allowances = ["/openshift-merge-robot", "osac-project/wg-infra", "osac-project/org-admins"] environments = [{ name = "e2e-test" }] @@ -209,7 +209,7 @@ module "repo_osac_installer" { ] required_status_checks = [ - { context = "e2e-vmaas-full-install / e2e", integration_id = 15368 }, + "e2e-vmaas-full-install / e2e" ] required_approvals = null From 3a4a59fc04a15212c5a9fbc1176edaaf87155d53 Mon Sep 17 00:00:00 2001 From: Riccardo Piccoli Date: Thu, 16 Jul 2026 16:12:44 +0200 Subject: [PATCH 2/2] NO-ISSUE: address CodeRabbit review findings - Scope GitHub App tokens to current repository in both plan and apply workflows (least privilege) - Add permission-contents: read to plan workflow token - Add tflint SAST step before tofu plan Assisted-by: Claude Code Signed-off-by: Riccardo Piccoli --- .github/workflows/apply.yaml | 1 + .github/workflows/plan.yaml | 6 ++++++ repositories.tf | 10 +++++----- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/apply.yaml b/.github/workflows/apply.yaml index 1a710f1..c1da8c0 100644 --- a/.github/workflows/apply.yaml +++ b/.github/workflows/apply.yaml @@ -44,6 +44,7 @@ jobs: client-id: ${{ env.CONFIG_APP_CLIENT_ID }} private-key: ${{ secrets.CONFIG_APP_SECRET }} owner: ${{ github.repository_owner }} + repositories: ${{ github.event.repository.name }} - name: Setup OpenTofu uses: opentofu/setup-opentofu@a1320f892987e89d278cc92dc5adc984fb93aca4 # v2 with: diff --git a/.github/workflows/plan.yaml b/.github/workflows/plan.yaml index d1c215d..6d9f970 100644 --- a/.github/workflows/plan.yaml +++ b/.github/workflows/plan.yaml @@ -38,6 +38,8 @@ jobs: client-id: ${{ env.CONFIG_APP_CLIENT_ID }} private-key: ${{ secrets.CONFIG_APP_SECRET }} owner: ${{ github.repository_owner }} + repositories: ${{ github.event.repository.name }} + permission-contents: read - name: Setup OpenTofu uses: opentofu/setup-opentofu@a1320f892987e89d278cc92dc5adc984fb93aca4 # v2 @@ -50,6 +52,10 @@ jobs: - name: TF Validate run: tofu validate + - name: TF Lint + uses: terraform-linters/setup-tflint@6e1e0642c0289bd619021bf6b34e3c08ed1e005a # v6.3.0 + - run: tflint --init && tflint + - name: TF Plan id: plan run: | diff --git a/repositories.tf b/repositories.tf index ba38719..9fb3027 100644 --- a/repositories.tf +++ b/repositories.tf @@ -36,7 +36,7 @@ module "repo_github_config" { use_public_template = false required_approvals = 2 required_status_checks = [ - "pre-commit", + { context = "pre-commit", integration_id = 15368 }, ] teams = [ @@ -107,7 +107,7 @@ module "repo_fulfillment_service" { ] required_approvals = null required_status_checks = [ - "e2e-vmaas-full-install / e2e" + { context = "e2e-vmaas-full-install / e2e", integration_id = 15368 }, ] push_allowances = ["/openshift-merge-robot", "osac-project/wg-infra", "osac-project/org-admins"] environments = [{ name = "e2e-test" }] @@ -137,7 +137,7 @@ module "repo_cloudkit_operator" { ] required_approvals = null required_status_checks = [ - "e2e-vmaas-full-install / e2e" + { context = "e2e-vmaas-full-install / e2e", integration_id = 15368 }, ] push_allowances = ["/openshift-merge-robot", "osac-project/wg-infra", "osac-project/org-admins"] environments = [{ name = "e2e-test" }] @@ -160,7 +160,7 @@ module "repo_cloudkit_aap" { ] required_approvals = null required_status_checks = [ - "e2e-vmaas-full-install / e2e" + { context = "e2e-vmaas-full-install / e2e", integration_id = 15368 }, ] push_allowances = ["/openshift-merge-robot", "osac-project/wg-infra", "osac-project/org-admins"] environments = [{ name = "e2e-test" }] @@ -209,7 +209,7 @@ module "repo_osac_installer" { ] required_status_checks = [ - "e2e-vmaas-full-install / e2e" + { context = "e2e-vmaas-full-install / e2e", integration_id = 15368 }, ] required_approvals = null