Repository: paularmstrong/build-tracker
Pull request #144 (2020-02-23 23:47:22)

[comparator] add * special key for artifact budgets
<!--
Thank you so much for contributing to open source and the Build Tracker project!
-->

# Problem

I want to be able to apply a budget to all artifacts, known and unknown without having to update my configuration every time a new artifact is created.

# Solution

Add a special key to the `artifacts.budgets` configuration, `'*'`, that will be added and applied to every artifact budget.

# TODO

- [x] 🤓 Add & update tests (always try to _increase_ test coverage)
- [x] 🔬 Ensure CI is passing (`yarn lint:ci`, `yarn test`, `yarn tsc:ci`)
- [x] 📖 Update relevant documentation



Patch:
diff --git a/config/fixtures-large.js b/config/fixtures-large.js
index be4a1c2..21ee9ec 100644
--- a/config/fixtures-large.js
+++ b/config/fixtures-large.js
@@ -3,16 +3,10 @@ const { BudgetLevel, BudgetType } = require('@build-tracker/types');
 
 module.exports = makeFixtureConfig('large', {
   artifacts: {
-    groups: [
-      {
-        artifactMatch: /^web\//,
-        name: 'Web'
-      },
-      {
-        artifactMatch: /^serviceworker\//,
-        name: 'Service Worker'
-      }
-    ],
+    budgets: {
+      '*': [{ level: BudgetLevel.WARN, sizeKey: 'gzip', type: BudgetType.PERCENT_DELTA, maximum: 0.5 }]
+    },
+    groups: [{ artifactMatch: /^web\//, name: 'Web' }, { artifactMatch: /^serviceworker\//, name: 'Service Worker' }],
     filters: [
       // Ignore all but english
       /web\/i18n-\w+\/(?!en\.).+/,
@@ -21,10 +15,7 @@ module.exports = makeFixtureConfig('large', {
       /web\/ondemand\.countries-(?!en\.).+/
     ]
   },
-  budgets: [
-    { level: BudgetLevel.WARN, sizeKey: 'gzip', type: BudgetType.PERCENT_DELTA, maximum: 0.5 },
-    { level: BudgetLevel.WARN, sizeKey: 'gzip', type: BudgetType.DELTA, maximum: 1000 }
-  ],
+  budgets: [{ level: BudgetLevel.WARN, sizeKey: 'gzip', type: BudgetType.DELTA, maximum: 1000 }],
   defaultSizeKey: 'gzip',
   name: 'Static Fixtures - Large Set'
 });
diff --git a/docs/docs/packages/server.md b/docs/docs/packages/server.md
index 25f1fb0..194fec7 100644
--- a/docs/docs/packages/server.md
+++ b/docs/docs/packages/server.md
@@ -64,6 +64,20 @@ module.exports = {
 };
 ```
 
+Artifact budgets can also be given a special key, `'*'`, that will apply to all artifacts in every build and be merged with any specific budgets:
+
+```js
+module.exports = {
+  artifacts: {
+    budgets: {
+      main: [{ level: 'error', sizeKey: 'gzip', type: 'size', maximum: 150000 }],
+      // Warn on any artifact that grows more than 50%
+      '*': [{ level: 'warn', sizeKey: 'gzip', type: 'percent_delta', maximum: 0.5 }]
+    }
+  }
+};
+```
+
 ##### `artifacts.filters?: Array<RegExp>`
 
 An array of regular expressions used to hide artifacts from display. This is typically not needed, unless you have artifacts generated by systems that really don't need to be checked.
diff --git a/src/comparator/src/BuildDelta.ts b/src/comparator/src/BuildDelta.ts
index 6c0987d..969b346 100644
--- a/src/comparator/src/BuildDelta.ts
+++ b/src/comparator/src/BuildDelta.ts
@@ -107,10 +107,11 @@ export default class BuildDelta<M extends BuildMeta = BuildMeta> {
 
         const sizes = baseArtifact ? baseArtifact.sizes : this._fauxArtifactSizes;
         const prevSizes = prevArtifact ? prevArtifact.sizes : this._fauxArtifactSizes;
+        const budgets = [...(this._artifactBudgets[artifactName] || []), ...(this._artifactBudgets['*'] || [])];
 
         const delta = new ArtifactDelta(
           artifactName,
-          this._artifactBudgets[artifactName] || [],
+          budgets,
           sizes,
           prevSizes,
           !baseArtifact || !prevArtifact || baseArtifact.hash !== prevArtifact.hash


Test patch:
diff --git a/src/comparator/src/__tests__/BuildDelta.test.ts b/src/comparator/src/__tests__/BuildDelta.test.ts
index 51bc44b..6566067 100644
--- a/src/comparator/src/__tests__/BuildDelta.test.ts
+++ b/src/comparator/src/__tests__/BuildDelta.test.ts
@@ -4,6 +4,7 @@
 import ArtifactDelta from '../ArtifactDelta';
 import Build from '@build-tracker/build';
 import BuildDelta from '../BuildDelta';
+import { BudgetLevel, BudgetType } from '@build-tracker/types';
 
 describe('BuildDelta', () => {
   let buildA, buildB;
@@ -111,6 +112,33 @@ describe('BuildDelta', () => {
         new ArtifactDelta('tacos', [], { gzip: 1, stat: 2 }, { gzip: 1, stat: 1 }, false)
       ]);
     });
+
+    test('includes budgets for each artifact', () => {
+      const budgets = [{ level: BudgetLevel.WARN, sizeKey: 'gzip', type: BudgetType.DELTA, maximum: 1 }];
+      const bd = new BuildDelta(buildA, buildB, {
+        artifactBudgets: { tacos: budgets }
+      });
+      expect(bd.artifactDeltas).toEqual(
+        expect.arrayContaining([new ArtifactDelta('tacos', budgets, { gzip: 1, stat: 2 }, { gzip: 1, stat: 1 }, false)])
+      );
+    });
+
+    test('includes * budgets on every artifact', () => {
+      const budget = { level: BudgetLevel.WARN, sizeKey: 'gzip', type: BudgetType.DELTA, maximum: 1 };
+      const burritoBudget = { level: BudgetLevel.ERROR, sizeKey: 'stat', type: BudgetType.DELTA, maximum: 1 };
+      const bd = new BuildDelta(buildA, buildB, { artifactBudgets: { '*': [budget], burritos: [burritoBudget] } });
+      expect(bd.artifactDeltas).toEqual([
+        new ArtifactDelta('tacos', [budget], { gzip: 1, stat: 2 }, { gzip: 1, stat: 1 }, false),
+        new ArtifactDelta(
+          'burritos',
+          expect.arrayContaining([budget, burritoBudget]),
+          { gzip: 2, stat: 3 },
+          { gzip: 4, stat: 6 },
+          true
+        ),
+        new ArtifactDelta('churros', [budget], { gzip: 0, stat: 0 }, { gzip: 4, stat: 6 }, true)
+      ]);
+    });
   });
 
   describe('getArtifactDelta', () => {