tfscout 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,48 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ test:
13
+ runs-on: ubuntu-latest
14
+ strategy:
15
+ matrix:
16
+ python: ["3.10", "3.11", "3.12", "3.13"]
17
+ steps:
18
+ - uses: actions/checkout@v7
19
+ - uses: actions/setup-python@v7
20
+ with:
21
+ python-version: ${{ matrix.python }}
22
+ - run: pip install -e '.[dev]'
23
+ - run: ruff check . && ruff format --check .
24
+ - run: pytest -q
25
+
26
+ action:
27
+ runs-on: ubuntu-latest
28
+ steps:
29
+ - uses: actions/checkout@v7
30
+ - id: review
31
+ uses: ./
32
+ with:
33
+ mode: review
34
+ plan: tests/fixtures/plan_review.json
35
+ provider: none
36
+ comment: "false"
37
+ - id: drift
38
+ uses: ./
39
+ with:
40
+ mode: drift
41
+ plan: tests/fixtures/plan_drift.json
42
+ provider: none
43
+ comment: "false"
44
+ - name: Check outputs
45
+ run: |
46
+ test "${{ steps.review.outputs.max-severity }}" = critical
47
+ test "${{ steps.drift.outputs.drift-detected }}" = true
48
+ test -s "${{ steps.review.outputs.report-file }}"
@@ -0,0 +1,47 @@
1
+ name: Publish to PyPI
2
+
3
+ # Runs when a GitHub release is published. Uses PyPI trusted publishing (OIDC), so no API token is stored.
4
+ # One-time setup on https://pypi.org/manage/account/publishing/ ("Add a new pending publisher"):
5
+ # PyPI project name: tfscout · Owner: mikonoid · Repository: tfscout
6
+ # Workflow: publish.yml · Environment: pypi
7
+ on:
8
+ release:
9
+ types: [published]
10
+
11
+ permissions:
12
+ contents: read
13
+
14
+ jobs:
15
+ build:
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/checkout@v7
19
+ - uses: actions/setup-python@v7
20
+ with:
21
+ python-version: "3.12"
22
+ - name: Check that the tag matches the package version
23
+ run: |
24
+ version=$(python -c "import re; print(re.search(r'__version__ = \"(.+)\"', open('src/tfscout/__init__.py').read())[1])")
25
+ tag="${GITHUB_REF_NAME#v}"
26
+ if [ "$version" != "$tag" ]; then
27
+ echo "::error::release tag v$tag does not match __version__ $version"
28
+ exit 1
29
+ fi
30
+ - run: pip install build && python -m build
31
+ - uses: actions/upload-artifact@v7
32
+ with:
33
+ name: dist
34
+ path: dist/
35
+
36
+ publish:
37
+ needs: build
38
+ runs-on: ubuntu-latest
39
+ environment: pypi
40
+ permissions:
41
+ id-token: write
42
+ steps:
43
+ - uses: actions/download-artifact@v8
44
+ with:
45
+ name: dist
46
+ path: dist/
47
+ - uses: pypa/gh-action-pypi-publish@v1.14.3
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ dist/
8
+ build/
9
+ .terraform/
10
+ *.tfplan
11
+ *.tfstate*
tfscout-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mikonoid
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
tfscout-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,194 @@
1
+ Metadata-Version: 2.5
2
+ Name: tfscout
3
+ Version: 0.1.0
4
+ Summary: AI-assisted Terraform plan review and drift detection
5
+ Project-URL: Homepage, https://github.com/mikonoid/tfscout
6
+ Project-URL: Issues, https://github.com/mikonoid/tfscout/issues
7
+ Author: mikonoid
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: devsecops,drift,github-action,iac,llm,opentofu,terraform
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Software Development :: Quality Assurance
13
+ Classifier: Topic :: System :: Systems Administration
14
+ Requires-Python: >=3.10
15
+ Provides-Extra: dev
16
+ Requires-Dist: pytest>=8; extra == 'dev'
17
+ Requires-Dist: ruff>=0.6; extra == 'dev'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # tfscout
21
+
22
+ **AI-assisted plan review and drift detection for Terraform / OpenTofu.**
23
+ Runs entirely in your CI: no SaaS, no server, and your plans never leave the runner unredacted.
24
+ **tfscout never touches your cloud.** It only reads the plan file your pipeline already produces.
25
+
26
+ 🧪 **[Live demo](https://github.com/mikonoid/tfscout-demo)**: see real PR comments and drift issues, no AWS account needed.
27
+
28
+ 📘 **[Setup guide](https://github.com/mikonoid/tfscout/blob/main/docs/setup.md)**: what you need, AWS OIDC in 10 minutes, Jenkins/GitLab, troubleshooting.
29
+
30
+ - 🔎 **`review`** reads `terraform plan` in a pull request and posts a single, auto-updated comment with what
31
+ changes, and what can break: data loss, open security groups, IAM wildcards, disabled encryption, forced
32
+ replacements, plus anything the AI notices.
33
+ - 🧭 **`drift`** reads `terraform plan -refresh-only` on a schedule, classifies every drifted resource as
34
+ `noise`, `manual-change` or `risky`, explains it and suggests how to codify or revert it. Results go to one
35
+ GitHub issue per root module, which is updated when drift changes and closed when it is resolved.
36
+
37
+ ```
38
+ plan ──► redact secrets ──► deterministic rules ──► LLM (optional) ──► PR comment / issue / JSON
39
+ ```
40
+
41
+ Deterministic rules are the baseline you can trust. The LLM adds a plain-English summary and extra findings,
42
+ and can escalate but **never downgrade** a risky item. With `provider: none` tfscout works without any AI.
43
+
44
+ ## Example PR comment
45
+
46
+ > ### 🔎 tfscout plan review — `prod`
47
+ > ➕ create **2** · ✏️ update **2** · ♻️ replace **1**
48
+ >
49
+ > - Recreates `orders-db` in another AZ
50
+ > - Grants CI full admin
51
+ >
52
+ > | | Severity | Resource | Finding |
53
+ > |---|---|---|---|
54
+ > | 🔴 | critical | `aws_db_instance.main` | Stateful resource will be REPLACED |
55
+ > | 🔴 | critical | `aws_iam_policy.ci` | IAM policy allows Action "*" |
56
+ > | 🔴 | critical | `aws_security_group.web` | Ingress opened to the internet (22/tcp) |
57
+ > | 🟠 | high | `aws_db_instance.main` | Encryption disabled (`storage_encrypted`) |
58
+ > | 🟠 | high | `aws_db_instance.main` | AZ move forces DB recreation 🤖 |
59
+
60
+ ## Quick start (GitHub Action)
61
+
62
+ Already running `terraform plan` in GitHub Actions? Add one step. No new cloud credentials are needed.
63
+ Otherwise see the [setup guide](https://github.com/mikonoid/tfscout/blob/main/docs/setup.md).
64
+
65
+ ### Pull request review
66
+
67
+ ```yaml
68
+ permissions:
69
+ contents: read
70
+ pull-requests: write
71
+ id-token: write # for cloud OIDC
72
+
73
+ steps:
74
+ - uses: actions/checkout@v7
75
+ - uses: hashicorp/setup-terraform@v4
76
+ with: { terraform_wrapper: false }
77
+ # ... configure cloud credentials ...
78
+ - run: terraform init -input=false && terraform plan -input=false -lock=false -out=tfplan
79
+ working-directory: envs/prod
80
+ - uses: mikonoid/tfscout@v0
81
+ with:
82
+ mode: review
83
+ working-directory: envs/prod
84
+ plan: tfplan
85
+ provider: gemini
86
+ api-key: ${{ secrets.GEMINI_API_KEY }}
87
+ fail-on: critical # optional merge gate
88
+ ```
89
+
90
+ ### Scheduled drift detection
91
+
92
+ ```yaml
93
+ on:
94
+ schedule: [{ cron: "0 6 * * 1-5" }]
95
+ permissions:
96
+ contents: read
97
+ issues: write
98
+ id-token: write
99
+
100
+ steps:
101
+ # ... checkout, setup-terraform, credentials, init ...
102
+ - run: terraform plan -refresh-only -input=false -lock=false -out=drift.tfplan
103
+ working-directory: envs/prod
104
+ - uses: mikonoid/tfscout@v0
105
+ with:
106
+ mode: drift
107
+ working-directory: envs/prod
108
+ plan: drift.tfplan
109
+ provider: gemini
110
+ api-key: ${{ secrets.GEMINI_API_KEY }}
111
+ ```
112
+
113
+ Full workflows with a matrix over several directories: [examples/review.yml](https://github.com/mikonoid/tfscout/blob/main/examples/review.yml),
114
+ [examples/drift.yml](https://github.com/mikonoid/tfscout/blob/main/examples/drift.yml).
115
+ One-time AWS OIDC role for CI without stored keys: [examples/aws-oidc](https://github.com/mikonoid/tfscout/blob/main/examples/aws-oidc/main.tf).
116
+
117
+ ### Inputs
118
+
119
+ | Input | Default | Description |
120
+ |---|---|---|
121
+ | `mode` | — | `review` or `drift` |
122
+ | `plan` | — | Binary plan (`-out`) or `terraform show -json` output, relative to `working-directory` |
123
+ | `working-directory` | `.` | Root module directory |
124
+ | `name` | working-directory | Label used in titles; keeps comments/issues per module separate |
125
+ | `provider` | `none` | `none`, `gemini`, `groq`, `openrouter`, `openai`, `ollama`, `custom` |
126
+ | `model` / `base-url` | preset | Override model (comma-separated = fallback chain on 503/429/timeout/404) / endpoint |
127
+ | `timeout` | `90` | Seconds to wait for the LLM per attempt |
128
+ | `reasoning-effort` | preset | `minimal`/`low`/`medium`/`high` for thinking models (gemini: `low`) |
129
+ | `api-key` | — | LLM API key (use a secret) |
130
+ | `comment` | `true` | Post PR comment (review) or sync the drift issue (drift) |
131
+ | `fail-on` | `none` | review: fail when a finding is at least this severity |
132
+ | `fail-on-drift` | `false` | drift: fail when actionable drift exists |
133
+ | `terraform-bin` | `terraform` | Use `tofu` for OpenTofu |
134
+
135
+ Outputs: `max-severity`, `findings-count`, `drift-detected`, `fingerprint`, `report-file`.
136
+ The report is also added to the job summary.
137
+
138
+ ## LLM providers
139
+
140
+ tfscout speaks the OpenAI-compatible `/chat/completions` API, so you can use any of these:
141
+
142
+ | Provider | Cost | Key |
143
+ |---|---|---|
144
+ | `gemini` (fallback chain `gemini-flash-lite-latest` → `gemini-3.5-flash` → `gemini-flash-latest`) | free tier | [AI Studio](https://aistudio.google.com/apikey) → `GEMINI_API_KEY` |
145
+ | `groq` | free tier | `GROQ_API_KEY` |
146
+ | `openrouter` | free models available | `OPENROUTER_API_KEY` |
147
+ | `openai` | paid | `OPENAI_API_KEY` |
148
+ | `ollama` | free, local/self-hosted runner | none |
149
+ | `custom` | — | any OpenAI-compatible endpoint (Azure AI Foundry, vLLM, LM Studio…) |
150
+
151
+ ## Security & privacy
152
+
153
+ - `terraform show -json` prints sensitive values **in clear text**. Before anything is sent to an LLM, tfscout
154
+ applies Terraform's own `*_sensitive` masks and also redacts values by key name (`password`, `token`,
155
+ `private_key`…) and by pattern (AWS keys, PEM keys, GitHub/Slack/OpenAI tokens, JWTs, DB URLs with
156
+ credentials).
157
+ - Only redacted attribute diffs are sent, never the full state.
158
+ - `provider: none` sends nothing anywhere.
159
+ - Use read-only cloud credentials for planning. tfscout never runs `apply`.
160
+
161
+ ## CLI
162
+
163
+ ```bash
164
+ pip install git+https://github.com/mikonoid/tfscout # PyPI package coming soon
165
+
166
+ terraform plan -out=tfplan
167
+ tfscout review tfplan # rules only
168
+ GEMINI_API_KEY=... tfscout review tfplan --provider gemini
169
+ tfscout review plan.json --format json --fail-on high # exit 3 on high/critical
170
+
171
+ terraform plan -refresh-only -out=drift.tfplan
172
+ tfscout drift drift.tfplan --provider ollama --exit-code # exit 2 on actionable drift
173
+ ```
174
+
175
+ Works with GitLab CI, Jenkins and locally too. Requires Python ≥ 3.10 and has no third-party dependencies.
176
+
177
+ ## Roadmap
178
+
179
+ - [ ] CloudTrail attribution for drift ("who changed it and when")
180
+ - [ ] Cost delta via Infracost
181
+ - [ ] Auto-generated PR that codifies a manual change
182
+ - [ ] Custom org rules (`.tfscout.yml`: required tags, allowed regions, naming)
183
+ - [ ] GCP/Azure rule coverage
184
+
185
+ ## Development
186
+
187
+ ```bash
188
+ python3 -m venv .venv && .venv/bin/pip install -e '.[dev]'
189
+ .venv/bin/pytest -q && .venv/bin/ruff check . && .venv/bin/ruff format --check .
190
+ ```
191
+
192
+ ## License
193
+
194
+ MIT
@@ -0,0 +1,175 @@
1
+ # tfscout
2
+
3
+ **AI-assisted plan review and drift detection for Terraform / OpenTofu.**
4
+ Runs entirely in your CI: no SaaS, no server, and your plans never leave the runner unredacted.
5
+ **tfscout never touches your cloud.** It only reads the plan file your pipeline already produces.
6
+
7
+ 🧪 **[Live demo](https://github.com/mikonoid/tfscout-demo)**: see real PR comments and drift issues, no AWS account needed.
8
+
9
+ 📘 **[Setup guide](https://github.com/mikonoid/tfscout/blob/main/docs/setup.md)**: what you need, AWS OIDC in 10 minutes, Jenkins/GitLab, troubleshooting.
10
+
11
+ - 🔎 **`review`** reads `terraform plan` in a pull request and posts a single, auto-updated comment with what
12
+ changes, and what can break: data loss, open security groups, IAM wildcards, disabled encryption, forced
13
+ replacements, plus anything the AI notices.
14
+ - 🧭 **`drift`** reads `terraform plan -refresh-only` on a schedule, classifies every drifted resource as
15
+ `noise`, `manual-change` or `risky`, explains it and suggests how to codify or revert it. Results go to one
16
+ GitHub issue per root module, which is updated when drift changes and closed when it is resolved.
17
+
18
+ ```
19
+ plan ──► redact secrets ──► deterministic rules ──► LLM (optional) ──► PR comment / issue / JSON
20
+ ```
21
+
22
+ Deterministic rules are the baseline you can trust. The LLM adds a plain-English summary and extra findings,
23
+ and can escalate but **never downgrade** a risky item. With `provider: none` tfscout works without any AI.
24
+
25
+ ## Example PR comment
26
+
27
+ > ### 🔎 tfscout plan review — `prod`
28
+ > ➕ create **2** · ✏️ update **2** · ♻️ replace **1**
29
+ >
30
+ > - Recreates `orders-db` in another AZ
31
+ > - Grants CI full admin
32
+ >
33
+ > | | Severity | Resource | Finding |
34
+ > |---|---|---|---|
35
+ > | 🔴 | critical | `aws_db_instance.main` | Stateful resource will be REPLACED |
36
+ > | 🔴 | critical | `aws_iam_policy.ci` | IAM policy allows Action "*" |
37
+ > | 🔴 | critical | `aws_security_group.web` | Ingress opened to the internet (22/tcp) |
38
+ > | 🟠 | high | `aws_db_instance.main` | Encryption disabled (`storage_encrypted`) |
39
+ > | 🟠 | high | `aws_db_instance.main` | AZ move forces DB recreation 🤖 |
40
+
41
+ ## Quick start (GitHub Action)
42
+
43
+ Already running `terraform plan` in GitHub Actions? Add one step. No new cloud credentials are needed.
44
+ Otherwise see the [setup guide](https://github.com/mikonoid/tfscout/blob/main/docs/setup.md).
45
+
46
+ ### Pull request review
47
+
48
+ ```yaml
49
+ permissions:
50
+ contents: read
51
+ pull-requests: write
52
+ id-token: write # for cloud OIDC
53
+
54
+ steps:
55
+ - uses: actions/checkout@v7
56
+ - uses: hashicorp/setup-terraform@v4
57
+ with: { terraform_wrapper: false }
58
+ # ... configure cloud credentials ...
59
+ - run: terraform init -input=false && terraform plan -input=false -lock=false -out=tfplan
60
+ working-directory: envs/prod
61
+ - uses: mikonoid/tfscout@v0
62
+ with:
63
+ mode: review
64
+ working-directory: envs/prod
65
+ plan: tfplan
66
+ provider: gemini
67
+ api-key: ${{ secrets.GEMINI_API_KEY }}
68
+ fail-on: critical # optional merge gate
69
+ ```
70
+
71
+ ### Scheduled drift detection
72
+
73
+ ```yaml
74
+ on:
75
+ schedule: [{ cron: "0 6 * * 1-5" }]
76
+ permissions:
77
+ contents: read
78
+ issues: write
79
+ id-token: write
80
+
81
+ steps:
82
+ # ... checkout, setup-terraform, credentials, init ...
83
+ - run: terraform plan -refresh-only -input=false -lock=false -out=drift.tfplan
84
+ working-directory: envs/prod
85
+ - uses: mikonoid/tfscout@v0
86
+ with:
87
+ mode: drift
88
+ working-directory: envs/prod
89
+ plan: drift.tfplan
90
+ provider: gemini
91
+ api-key: ${{ secrets.GEMINI_API_KEY }}
92
+ ```
93
+
94
+ Full workflows with a matrix over several directories: [examples/review.yml](https://github.com/mikonoid/tfscout/blob/main/examples/review.yml),
95
+ [examples/drift.yml](https://github.com/mikonoid/tfscout/blob/main/examples/drift.yml).
96
+ One-time AWS OIDC role for CI without stored keys: [examples/aws-oidc](https://github.com/mikonoid/tfscout/blob/main/examples/aws-oidc/main.tf).
97
+
98
+ ### Inputs
99
+
100
+ | Input | Default | Description |
101
+ |---|---|---|
102
+ | `mode` | — | `review` or `drift` |
103
+ | `plan` | — | Binary plan (`-out`) or `terraform show -json` output, relative to `working-directory` |
104
+ | `working-directory` | `.` | Root module directory |
105
+ | `name` | working-directory | Label used in titles; keeps comments/issues per module separate |
106
+ | `provider` | `none` | `none`, `gemini`, `groq`, `openrouter`, `openai`, `ollama`, `custom` |
107
+ | `model` / `base-url` | preset | Override model (comma-separated = fallback chain on 503/429/timeout/404) / endpoint |
108
+ | `timeout` | `90` | Seconds to wait for the LLM per attempt |
109
+ | `reasoning-effort` | preset | `minimal`/`low`/`medium`/`high` for thinking models (gemini: `low`) |
110
+ | `api-key` | — | LLM API key (use a secret) |
111
+ | `comment` | `true` | Post PR comment (review) or sync the drift issue (drift) |
112
+ | `fail-on` | `none` | review: fail when a finding is at least this severity |
113
+ | `fail-on-drift` | `false` | drift: fail when actionable drift exists |
114
+ | `terraform-bin` | `terraform` | Use `tofu` for OpenTofu |
115
+
116
+ Outputs: `max-severity`, `findings-count`, `drift-detected`, `fingerprint`, `report-file`.
117
+ The report is also added to the job summary.
118
+
119
+ ## LLM providers
120
+
121
+ tfscout speaks the OpenAI-compatible `/chat/completions` API, so you can use any of these:
122
+
123
+ | Provider | Cost | Key |
124
+ |---|---|---|
125
+ | `gemini` (fallback chain `gemini-flash-lite-latest` → `gemini-3.5-flash` → `gemini-flash-latest`) | free tier | [AI Studio](https://aistudio.google.com/apikey) → `GEMINI_API_KEY` |
126
+ | `groq` | free tier | `GROQ_API_KEY` |
127
+ | `openrouter` | free models available | `OPENROUTER_API_KEY` |
128
+ | `openai` | paid | `OPENAI_API_KEY` |
129
+ | `ollama` | free, local/self-hosted runner | none |
130
+ | `custom` | — | any OpenAI-compatible endpoint (Azure AI Foundry, vLLM, LM Studio…) |
131
+
132
+ ## Security & privacy
133
+
134
+ - `terraform show -json` prints sensitive values **in clear text**. Before anything is sent to an LLM, tfscout
135
+ applies Terraform's own `*_sensitive` masks and also redacts values by key name (`password`, `token`,
136
+ `private_key`…) and by pattern (AWS keys, PEM keys, GitHub/Slack/OpenAI tokens, JWTs, DB URLs with
137
+ credentials).
138
+ - Only redacted attribute diffs are sent, never the full state.
139
+ - `provider: none` sends nothing anywhere.
140
+ - Use read-only cloud credentials for planning. tfscout never runs `apply`.
141
+
142
+ ## CLI
143
+
144
+ ```bash
145
+ pip install git+https://github.com/mikonoid/tfscout # PyPI package coming soon
146
+
147
+ terraform plan -out=tfplan
148
+ tfscout review tfplan # rules only
149
+ GEMINI_API_KEY=... tfscout review tfplan --provider gemini
150
+ tfscout review plan.json --format json --fail-on high # exit 3 on high/critical
151
+
152
+ terraform plan -refresh-only -out=drift.tfplan
153
+ tfscout drift drift.tfplan --provider ollama --exit-code # exit 2 on actionable drift
154
+ ```
155
+
156
+ Works with GitLab CI, Jenkins and locally too. Requires Python ≥ 3.10 and has no third-party dependencies.
157
+
158
+ ## Roadmap
159
+
160
+ - [ ] CloudTrail attribution for drift ("who changed it and when")
161
+ - [ ] Cost delta via Infracost
162
+ - [ ] Auto-generated PR that codifies a manual change
163
+ - [ ] Custom org rules (`.tfscout.yml`: required tags, allowed regions, naming)
164
+ - [ ] GCP/Azure rule coverage
165
+
166
+ ## Development
167
+
168
+ ```bash
169
+ python3 -m venv .venv && .venv/bin/pip install -e '.[dev]'
170
+ .venv/bin/pytest -q && .venv/bin/ruff check . && .venv/bin/ruff format --check .
171
+ ```
172
+
173
+ ## License
174
+
175
+ MIT
@@ -0,0 +1,115 @@
1
+ name: tfscout
2
+ description: AI-assisted Terraform/OpenTofu plan review and drift detection. Explains risks in PR comments and drift in issues.
3
+ author: mikonoid
4
+ branding:
5
+ icon: search
6
+ color: purple
7
+
8
+ inputs:
9
+ mode:
10
+ description: "`review` (comment on a PR with plan risks) or `drift` (analyse a `plan -refresh-only` and sync an issue)"
11
+ required: true
12
+ plan:
13
+ description: Path to the plan file (binary `-out` file or `terraform show -json` output), relative to working-directory
14
+ required: true
15
+ working-directory:
16
+ description: Terraform root module directory (needed to read binary plans)
17
+ default: "."
18
+ name:
19
+ description: Label for this root module, used in comment/issue titles and to keep them separate (defaults to working-directory)
20
+ default: ""
21
+ provider:
22
+ description: "LLM provider: none (rules only), gemini (free tier), groq, openrouter, openai, ollama, custom"
23
+ default: none
24
+ model:
25
+ description: Override the model name
26
+ default: ""
27
+ base-url:
28
+ description: Override the OpenAI-compatible endpoint (required for provider `custom`)
29
+ default: ""
30
+ api-key:
31
+ description: API key for the LLM provider (pass it from a secret)
32
+ default: ""
33
+ timeout:
34
+ description: Seconds to wait for the LLM per attempt
35
+ default: "90"
36
+ reasoning-effort:
37
+ description: "reasoning_effort for thinking models: minimal|low|medium|high (gemini default: low)"
38
+ default: ""
39
+ github-token:
40
+ description: Token used for PR comments and drift issues
41
+ default: ${{ github.token }}
42
+ comment:
43
+ description: "review: create/update a PR comment. drift: create/update/close a drift issue"
44
+ default: "true"
45
+ fail-on:
46
+ description: "review: fail the step when a finding has at least this severity (none|info|low|medium|high|critical)"
47
+ default: none
48
+ fail-on-drift:
49
+ description: "drift: fail the step when actionable drift is found"
50
+ default: "false"
51
+ terraform-bin:
52
+ description: terraform or tofu binary used to read binary plans
53
+ default: terraform
54
+
55
+ outputs:
56
+ max-severity:
57
+ description: "review: highest finding severity (or `none`)"
58
+ value: ${{ steps.run.outputs.max-severity }}
59
+ findings-count:
60
+ description: Number of findings (review) or actionable drift items (drift)
61
+ value: ${{ steps.run.outputs.findings-count }}
62
+ drift-detected:
63
+ description: "drift: `true` when actionable drift was found"
64
+ value: ${{ steps.run.outputs.drift-detected }}
65
+ fingerprint:
66
+ description: "drift: stable id of the current drift"
67
+ value: ${{ steps.run.outputs.fingerprint }}
68
+ report-file:
69
+ description: Path to the rendered Markdown report
70
+ value: ${{ steps.run.outputs.report-file }}
71
+
72
+ runs:
73
+ using: composite
74
+ steps:
75
+ - id: run
76
+ shell: bash
77
+ working-directory: ${{ inputs.working-directory }}
78
+ env:
79
+ PYTHONPATH: ${{ github.action_path }}/src
80
+ IN_MODE: ${{ inputs.mode }}
81
+ IN_PLAN: ${{ inputs.plan }}
82
+ IN_NAME: ${{ inputs.name || inputs.working-directory }}
83
+ IN_PROVIDER: ${{ inputs.provider }}
84
+ IN_MODEL: ${{ inputs.model }}
85
+ IN_BASE_URL: ${{ inputs.base-url }}
86
+ IN_COMMENT: ${{ inputs.comment }}
87
+ IN_FAIL_ON: ${{ inputs.fail-on }}
88
+ IN_FAIL_ON_DRIFT: ${{ inputs.fail-on-drift }}
89
+ IN_TF_BIN: ${{ inputs.terraform-bin }}
90
+ TFSCOUT_API_KEY: ${{ inputs.api-key }}
91
+ TFSCOUT_TIMEOUT: ${{ inputs.timeout }}
92
+ TFSCOUT_REASONING_EFFORT: ${{ inputs.reasoning-effort }}
93
+ GITHUB_TOKEN: ${{ inputs.github-token }}
94
+ run: |
95
+ set -euo pipefail
96
+ if ! python3 -c 'import sys; sys.exit(sys.version_info < (3, 10))' 2>/dev/null; then
97
+ echo "::error::tfscout needs python3 >= 3.10 on the runner (add actions/setup-python)"
98
+ exit 1
99
+ fi
100
+ case "$IN_MODE" in review|drift) ;; *) echo "::error::mode must be 'review' or 'drift'"; exit 1 ;; esac
101
+
102
+ report="$RUNNER_TEMP/tfscout-${IN_MODE}-$(printf '%s' "$IN_NAME" | tr -c 'A-Za-z0-9._-' '_').md"
103
+ args=("$IN_MODE" "$IN_PLAN" --name "$IN_NAME" --provider "$IN_PROVIDER" --terraform-bin "$IN_TF_BIN" --output "$report")
104
+ [[ -n "$IN_MODEL" ]] && args+=(--model "$IN_MODEL")
105
+ [[ -n "$IN_BASE_URL" ]] && args+=(--base-url "$IN_BASE_URL")
106
+ if [[ "$IN_MODE" == review ]]; then
107
+ [[ "$IN_COMMENT" == true ]] && args+=(--github-comment)
108
+ args+=(--fail-on "$IN_FAIL_ON")
109
+ else
110
+ [[ "$IN_COMMENT" == true ]] && args+=(--github-issue)
111
+ [[ "$IN_FAIL_ON_DRIFT" == true ]] && args+=(--exit-code)
112
+ fi
113
+
114
+ echo "report-file=$report" >> "$GITHUB_OUTPUT"
115
+ python3 -m tfscout "${args[@]}"