@aaa-sentinel/iac-cost 0.0.0-stage → 0.1.0

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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +188 -3
  3. package/dist/cli.js +4522 -0
  4. package/package.json +41 -6
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AAA Sentinel
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.
package/README.md CHANGED
@@ -1,3 +1,188 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # @aaa-sentinel/iac-cost
2
+
3
+ Comments the **monthly cost delta of a Terraform plan** on the pull request that introduces it,
4
+ so the price of a change is visible while it is still cheap to change.
5
+
6
+ > ### 💲 Infrastructure cost estimate
7
+ >
8
+ > **Coverage: 3 of 8 billable resources priced** (4 more have no cost by design).
9
+ >
10
+ > **Estimated change: +$74.46/mo** (list price, USD)
11
+ >
12
+ > | Resource | Change | Qty | Delta |
13
+ > | --- | --- | --- | --- |
14
+ > | `aws_instance` | new | 2 | +$121.18/mo |
15
+ > | `azurerm_linux_virtual_machine` | removed | 1 | -$46.72/mo |
16
+ >
17
+ > <details><summary>Not priced (5 resources)</summary></details>
18
+
19
+ ## Requirements
20
+
21
+ - Node.js 20 or newer.
22
+ - An OAuth2 application with the `iac:estimate` scope, created in the developer portal.
23
+ That scope grants **nothing else** — it cannot read your accounts, findings or reports —
24
+ which is what makes it safe to paste into a CI that third parties can trigger.
25
+ - A **Pro** or **Business** plan.
26
+
27
+ ## Usage
28
+
29
+ ```bash
30
+ terraform plan -out=plan.tfplan
31
+ terraform show -json plan.tfplan > plan.json
32
+
33
+ npx @aaa-sentinel/iac-cost plan.json # or: … | npx @aaa-sentinel/iac-cost
34
+ ```
35
+
36
+ | Variable | Required | Meaning |
37
+ | --- | --- | --- |
38
+ | `IAC_COST_CLIENT_ID` | yes | Client ID of the OAuth2 application |
39
+ | `IAC_COST_CLIENT_SECRET` | yes | Client secret. Store it as an encrypted CI secret |
40
+ | `IAC_COST_BASE_URL` | no | Defaults to `https://api.aaa-sentinel.com`. Must be `https` — the secret is never sent over plain HTTP |
41
+
42
+ The Markdown comment goes to **stdout**; warnings go to **stderr**. If there is nothing to
43
+ say, stdout is empty.
44
+
45
+ ### It never fails your build
46
+
47
+ The exit code is **always 0**. A missing credential, a network error, an unreadable plan or an
48
+ outage on our side prints a one-line warning and stops. A cost comment is not a quality gate, and
49
+ our availability must not become a dependency of your deployments.
50
+
51
+ ## What actually leaves your machine
52
+
53
+ Not the plan. `terraform show -json` output carries CIDRs, account identifiers and, often,
54
+ secrets in clear text. Before anything is sent, the plan is reduced **on your machine** to a
55
+ whitelist: resource type, action (`create`/`update`/`delete`/`replace`), how many instances the
56
+ block produces, and the handful of attributes that move the price (instance size, region, OS).
57
+ Everything else is dropped before serialising, and the projection is never written to the CI log.
58
+
59
+ ## Reading the number
60
+
61
+ - **List price.** Neither AWS, Azure nor Google returns what *you* pay. Enterprise agreements
62
+ (EDP/EA/CUD) and existing reservations or Savings Plans are invisible at pull request time —
63
+ a reservation that already covers the new resource makes the real marginal cost zero.
64
+ - **Priced in USD, shown in your organization's currency.** USD is what the providers publish, and
65
+ the API always returns it (`...Usd` fields). If your AAA Sentinel organization displays amounts
66
+ in EUR or GBP, the comment shows the converted amount instead, marked with `≈` and with the ECB
67
+ reference rate and its date spelled out — it is an estimate on top of an estimate, and the same
68
+ unchanged pull request can move a little as the exchange rate does. Organizations in USD, or a
69
+ day with no exchange rate available, get plain USD.
70
+ - **Coverage leads, not the total.** When fewer than half of the billable resources can be
71
+ priced, the breakdown is published but the total is withheld. A total built from 2 resources
72
+ out of 30 is not a partial estimate, it is noise — and it would end up quoted in the review as
73
+ if it were the figure.
74
+ - **An unknown region is treated as an unsupported one.** In AWS the region lives in the
75
+ `provider` block rather than the resource, so `terraform show -json` often leaves it unknown.
76
+ We say we do not know rather than substituting another region's price.
77
+
78
+ Currently priced: **AWS** (EC2 on demand) and **Azure** (virtual machines). Anything else is
79
+ reported as not priced, never as free.
80
+
81
+ ## GitHub Actions
82
+
83
+ Posts one comment per pull request and **updates it in place** on later pushes instead of
84
+ stacking a new one each time. The token is the one the run already injects — no personal access
85
+ token, no app installation.
86
+
87
+ ```yaml
88
+ name: Infrastructure cost
89
+ on: pull_request
90
+
91
+ permissions:
92
+ contents: read
93
+ pull-requests: write
94
+
95
+ jobs:
96
+ cost:
97
+ runs-on: ubuntu-latest
98
+ steps:
99
+ - uses: actions/checkout@v4
100
+ - uses: hashicorp/setup-terraform@v3
101
+ - uses: actions/setup-node@v4
102
+ with:
103
+ node-version: 20
104
+
105
+ - run: terraform init
106
+ - run: terraform plan -out=plan.tfplan
107
+ - run: terraform show -json plan.tfplan > plan.json
108
+
109
+ - name: Estimate cost
110
+ env:
111
+ IAC_COST_CLIENT_ID: ${{ secrets.IAC_COST_CLIENT_ID }}
112
+ IAC_COST_CLIENT_SECRET: ${{ secrets.IAC_COST_CLIENT_SECRET }}
113
+ run: npx --yes @aaa-sentinel/iac-cost plan.json > comment.md
114
+
115
+ - name: Comment on the pull request
116
+ continue-on-error: true
117
+ env:
118
+ GH_TOKEN: ${{ github.token }}
119
+ PR: ${{ github.event.number }}
120
+ run: |
121
+ [ -s comment.md ] || exit 0
122
+ id=$(gh api --paginate "repos/$GITHUB_REPOSITORY/issues/$PR/comments" \
123
+ --jq '.[] | select((.body // "") | contains("<!-- aaa-sentinel:iac-cost -->")) | .id' \
124
+ | head -n1)
125
+ if [ -n "$id" ]; then
126
+ gh api -X PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$id" -F body=@comment.md
127
+ else
128
+ gh api -X POST "repos/$GITHUB_REPOSITORY/issues/$PR/comments" -F body=@comment.md
129
+ fi
130
+ ```
131
+
132
+ > `continue-on-error` is not decoration: without it, a GitHub API hiccup while posting the comment
133
+ > would fail the job, and a cost comment must never be able to do that.
134
+
135
+ > On a pull request from a fork, GitHub gives the run a read-only token, so the comment step is
136
+ > skipped. That is a GitHub restriction and it applies to every tool that comments on pull
137
+ > requests, not just this one.
138
+
139
+ ## Bitbucket Pipelines
140
+
141
+ Bitbucket does not inject a token that can write to the repository, so create a **Repository
142
+ Access Token** with the `pullrequest:write` scope and store it as a secured repository variable
143
+ named `BB_TOKEN`.
144
+
145
+ ```yaml
146
+ pipelines:
147
+ pull-requests:
148
+ '**':
149
+ - step:
150
+ name: Infrastructure cost
151
+ image: hashicorp/terraform:latest
152
+ script:
153
+ - apk add --no-cache nodejs npm curl jq
154
+ - terraform init
155
+ - terraform plan -out=plan.tfplan
156
+ - terraform show -json plan.tfplan > plan.json
157
+ - npx --yes @aaa-sentinel/iac-cost plan.json > comment.md
158
+ - |
159
+ set +e
160
+ [ -s comment.md ] || exit 0
161
+ API="https://api.bitbucket.org/2.0/repositories/$BITBUCKET_WORKSPACE/$BITBUCKET_REPO_SLUG/pullrequests/$BITBUCKET_PR_ID/comments"
162
+ BODY=$(jq -Rs . < comment.md)
163
+ ID=$(curl -sf -H "Authorization: Bearer $BB_TOKEN" "$API?pagelen=100" \
164
+ | jq -r '[.values[] | select((.content.raw // "") | contains("<!-- aaa-sentinel:iac-cost -->"))][0].id // empty')
165
+ if [ -n "$ID" ]; then
166
+ curl -sf -X PUT -H "Authorization: Bearer $BB_TOKEN" -H 'Content-Type: application/json' \
167
+ -d "{\"content\":{\"raw\":$BODY}}" "$API/$ID"
168
+ else
169
+ curl -sf -X POST -H "Authorization: Bearer $BB_TOKEN" -H 'Content-Type: application/json' \
170
+ -d "{\"content\":{\"raw\":$BODY}}" "$API"
171
+ fi
172
+ exit 0
173
+ ```
174
+
175
+ > The `set +e` and the final `exit 0` are there for the same reason as `continue-on-error` on
176
+ > GitHub: a failure while commenting must not fail the pipeline. A deleted comment has a null
177
+ > `content.raw`, hence the `// ""` — without it `jq` aborts and the comment is posted again
178
+ > instead of updated.
179
+
180
+ ## Why there is no comment
181
+
182
+ | What you see | Why |
183
+ | --- | --- |
184
+ | No comment at all | The plan changes no billable resource, or the credentials are missing — check the step log, the reason is always printed |
185
+ | A breakdown with no total | Fewer than half of the billable resources could be priced |
186
+ | `region could not be resolved from the plan` | The region comes from a variable or an aliased provider, so the plan does not say which one applies |
187
+ | `provider recognized, pricing not connected yet` | A cloud we support but whose catalogue is not connected yet (Google) |
188
+ | `no matching price found in the catalog` | The size exists in the plan but not in that region's catalogue — an instance type not offered there, or a resource we price only for some families |