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

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