plutus-scan 0.1.0 → 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.
package/README.md CHANGED
@@ -202,6 +202,17 @@ For one of them, gp2 volumes, Plutus can open a pull request against your Terraf
202
202
 
203
203
  plutus-scan is a snapshot of one bill, taken once. [Plutus](https://plutus-cloud.com/?utm_source=plutus-scan&utm_medium=readme) tracks the same spend daily, per team and per customer, with alerts, and is where the questions this tool leaves open get answered: who owns this AI project, is this candidate actually waste, what happens tomorrow. plutus-scan has no paid tier, license key or upload step, and works fully without it. The pull-request line on the gp2 candidate, and the link in the footer of every HTML and Markdown report, are the same kind of pointer: a link you can follow or ignore, never a request plutus-scan makes. It still never contacts plutus-cloud.com.
204
204
 
205
+ ## Releasing
206
+
207
+ Releases are built and uploaded to npm by [`.github/workflows/release.yml`](.github/workflows/release.yml) when a GitHub release is published, not from a laptop. The workflow only stages the version; a maintainer makes it public with 2FA.
208
+
209
+ 1. Bump `version` in `package.json` and `package-lock.json` with `npm version 0.1.1 --no-git-tag-version`, open a pull request and merge it to `main`.
210
+ 2. On GitHub, draft a new release from `main` with a new tag named `v` plus the version (`v0.1.1`), write the notes and publish it. A version with a suffix such as `0.2.0-rc.1` is published under the npm `next` tag instead of `latest`.
211
+ 3. The workflow checks that the tag matches `package.json`, runs `npm ci` and `npm test`, checks that the tarball holds only `bin`, `src`, `LICENSE`, `package.json` and `README.md`, and stages it on npm with provenance (`npm stage publish`). If any check fails nothing is staged: fix it, delete the release and its tag, and publish again.
212
+ 4. Approve it from a machine signed in to npm with 2FA: `npm stage list plutus-scan` shows the staged version and its id, `npm stage download <stage-id>` fetches the tarball if you want to inspect it, and `npm stage approve <stage-id>` publishes it. `npm stage reject <stage-id>` throws it away. The staging commands need npm 12 or later.
213
+
214
+ One-time setup: the workflow uses npm trusted publishing, so there is no npm token to store. On npmjs.com, under the package's Settings, add a trusted publisher for GitHub Actions with organization `plutus-cloud`, repository `plutus-scan`, workflow `release.yml` and environment `npm`, and leave the options to publish directly and to manage dist-tags unchecked, so the workflow can only stage. Then set the package's publishing access to require two-factor authentication and disallow tokens.
215
+
205
216
  ## License
206
217
 
207
218
  MIT. See `LICENSE`. The FOCUS sample used as a test fixture is CC BY 4.0, credited in `test/fixtures/focus/official-sample.NOTICE.md`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plutus-scan",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Zero-setup CLI that reports what changed in a cloud or AI bill",
5
5
  "keywords": [
6
6
  "finops",
@@ -44,6 +44,9 @@ const COMMITMENT_TYPES = new Set(COMMITMENT_RECORD_TYPES.map(norm));
44
44
  const RESERVATION_USAGE = /(^|[-:])HeavyUsage|ReservedHostUsage/;
45
45
  /** Cost Explorer expression that leaves out the commitment record types. */
46
46
  const NOT_COMMITMENT = { Not: { Dimensions: { Key: 'RECORD_TYPE', Values: COMMITMENT_RECORD_TYPES } } };
47
+ /** The commitment fees plus the record types that never carry a tag. */
48
+ const NOT_TAGGABLE = { Not: { Dimensions: { Key: 'RECORD_TYPE',
49
+ Values: [...COMMITMENT_RECORD_TYPES, 'SavingsPlanNegation', 'Tax', 'Credit', 'Refund'] } } };
47
50
 
48
51
  /**
49
52
  * Kind and readable service label for one daily SERVICE x RECORD_TYPE group. Savings Plan fees keep
@@ -82,13 +85,13 @@ export function shellQuote(arg) {
82
85
  const period = (p) => `Start=${p.start},End=${addDays(p.end, 1)}`;
83
86
  const dim = (key) => `Type=DIMENSION,Key=${key}`;
84
87
 
85
- /** @param {{start:string,end:string}} p @param {'DAILY'|'MONTHLY'} granularity @param {string[]} groupBy @param {string|null} [filter] @param {string|null} [token] */
86
- function costArgs(p, granularity, groupBy, filter = null, token = null) {
88
+ /** @param {{start:string,end:string}} p @param {'DAILY'|'MONTHLY'} granularity @param {string[]} groupBy @param {string|null} [filter] @param {string|null} [token] @param {'UnblendedCost'|'AmortizedCost'} [metric] */
89
+ function costArgs(p, granularity, groupBy, filter = null, token = null, metric = 'UnblendedCost') {
87
90
  return [
88
91
  'ce', 'get-cost-and-usage',
89
92
  '--time-period', period(p),
90
93
  '--granularity', granularity,
91
- '--metrics', 'UnblendedCost',
94
+ '--metrics', metric,
92
95
  ...(filter ? ['--filter', filter] : []),
93
96
  '--group-by', ...groupBy,
94
97
  ...(token ? ['--next-page-token', token] : []),
@@ -113,9 +116,9 @@ export const candidateArgs = (/** @type {Window} */ w, token = null) =>
113
116
 
114
117
  export const tagListArgs = () => ['ce', 'list-cost-allocation-tags', '--status', 'Active', '--type', 'UserDefined'];
115
118
 
116
- /** Usage only, like FOCUS tag coverage: commitment fees are left out of the untagged share. */
119
+ /** Usage only, and AmortizedCost unlike every other query: see docs/design.md, step 5. */
117
120
  export const tagArgs = (/** @type {Window} */ w, /** @type {string} */ key, token = null) =>
118
- costArgs(candidatePeriod(w), 'MONTHLY', [`Type=TAG,Key=${key}`], JSON.stringify(NOT_COMMITMENT), token);
121
+ costArgs(candidatePeriod(w), 'MONTHLY', [`Type=TAG,Key=${key}`], JSON.stringify(NOT_TAGGABLE), token, 'AmortizedCost');
119
122
 
120
123
  /** @param {Window} w */
121
124
  const clampStart = (w, n) => {
@@ -244,7 +247,7 @@ export async function fetchAws(transport, { window, maxTagKeys = 5, maxRequests
244
247
 
245
248
  /** @param {any} g @returns {{ cost: number, currency: string }|null} null for a zero or missing amount */
246
249
  const amount = (g) => {
247
- const m = g.Metrics?.UnblendedCost;
250
+ const m = g.Metrics?.UnblendedCost ?? g.Metrics?.AmortizedCost;
248
251
  const cost = Number(m?.Amount);
249
252
  return !Number.isFinite(cost) || cost === 0 ? null : { cost, currency: currencyCode(m.Unit || 'USD') };
250
253
  };