From 32d397348996ebe580a9eda4a0506f56728a680a Mon Sep 17 00:00:00 2001
From: Oleg Krivtsov <olegkrivtsov@gmail.com>
Date: Mon, 22 Nov 2021 15:03:38 +0700
Subject: [PATCH] feat(manager/travis): support matrix node.js syntax in Travis
 configs (#12656)

Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
---
 .../travis/__fixtures__/matrix_alias.yml      |  5 ++
 .../travis/__fixtures__/matrix_invalid.yml    |  4 +
 .../travis/__fixtures__/matrix_jobs.yml       |  5 ++
 .../travis/__fixtures__/matrix_jobs_array.yml |  5 ++
 .../__fixtures__/matrix_jobs_array2.yml       |  7 ++
 lib/manager/travis/extract.spec.ts            | 82 ++++++++++++++++++-
 lib/manager/travis/extract.ts                 | 43 +++++++++-
 lib/manager/travis/readme.md                  |  8 +-
 lib/manager/travis/types.ts                   | 19 +++++
 9 files changed, 169 insertions(+), 9 deletions(-)
 create mode 100644 lib/manager/travis/__fixtures__/matrix_alias.yml
 create mode 100644 lib/manager/travis/__fixtures__/matrix_invalid.yml
 create mode 100644 lib/manager/travis/__fixtures__/matrix_jobs.yml
 create mode 100644 lib/manager/travis/__fixtures__/matrix_jobs_array.yml
 create mode 100644 lib/manager/travis/__fixtures__/matrix_jobs_array2.yml
 create mode 100644 lib/manager/travis/types.ts

diff --git a/lib/manager/travis/__fixtures__/matrix_alias.yml b/lib/manager/travis/__fixtures__/matrix_alias.yml
new file mode 100644
index 0000000000..0ec43788f5
--- /dev/null
+++ b/lib/manager/travis/__fixtures__/matrix_alias.yml
@@ -0,0 +1,5 @@
+matrix:
+  include:
+    - env: js-tests
+      language: node_js
+      node_js: '11.10.1'
diff --git a/lib/manager/travis/__fixtures__/matrix_invalid.yml b/lib/manager/travis/__fixtures__/matrix_invalid.yml
new file mode 100644
index 0000000000..a13a252e27
--- /dev/null
+++ b/lib/manager/travis/__fixtures__/matrix_invalid.yml
@@ -0,0 +1,4 @@
+jobs:
+  include:
+    - invalid: '1.0'
+
diff --git a/lib/manager/travis/__fixtures__/matrix_jobs.yml b/lib/manager/travis/__fixtures__/matrix_jobs.yml
new file mode 100644
index 0000000000..62139533e2
--- /dev/null
+++ b/lib/manager/travis/__fixtures__/matrix_jobs.yml
@@ -0,0 +1,5 @@
+jobs:
+  include:
+    - env: js-tests
+      language: node_js
+      node_js: '11.10.1'
diff --git a/lib/manager/travis/__fixtures__/matrix_jobs_array.yml b/lib/manager/travis/__fixtures__/matrix_jobs_array.yml
new file mode 100644
index 0000000000..c379d5a128
--- /dev/null
+++ b/lib/manager/travis/__fixtures__/matrix_jobs_array.yml
@@ -0,0 +1,5 @@
+jobs:
+  include:
+    - env: js-tests
+      language: node_js
+      node_js: ['11.10.1', '11.10.2']
diff --git a/lib/manager/travis/__fixtures__/matrix_jobs_array2.yml b/lib/manager/travis/__fixtures__/matrix_jobs_array2.yml
new file mode 100644
index 0000000000..58b70eff6d
--- /dev/null
+++ b/lib/manager/travis/__fixtures__/matrix_jobs_array2.yml
@@ -0,0 +1,7 @@
+jobs: 
+  include: 
+    - env: js-tests 
+      language: node_js 
+      node_js: 
+        - '11.10.1' 
+        - '11.10.2'
diff --git a/lib/manager/travis/extract.spec.ts b/lib/manager/travis/extract.spec.ts
index abeb3a98e4..1488759e58 100644
--- a/lib/manager/travis/extract.spec.ts
+++ b/lib/manager/travis/extract.spec.ts
@@ -1,7 +1,12 @@
 import { loadFixture } from '../../../test/util';
-import { extractPackageFile } from './extract';
+import { extractPackageFile } from '.';
 
 const invalidYAML = loadFixture('invalid.yml');
+const matrixYAMLwithNodeSyntaxString = loadFixture('matrix_jobs.yml');
+const matrixYAMLwithNodeSyntaxArray = loadFixture('matrix_jobs_array.yml');
+const matrixYAMLwithNodeSyntaxArray2 = loadFixture('matrix_jobs_array2.yml');
+const matrixYAMLwithNodeSyntaxAlias = loadFixture('matrix_alias.yml');
+const invalidMatrixYAML = loadFixture('matrix_invalid.yml');
 
 describe('manager/travis/extract', () => {
   describe('extractPackageFile()', () => {
@@ -9,14 +14,89 @@ describe('manager/travis/extract', () => {
       const res = extractPackageFile('blahhhhh:foo:@what\n');
       expect(res).toBeNull();
     });
+
     it('returns results', () => {
       const res = extractPackageFile('node_js:\n  - 6\n  - 8\n');
       expect(res).toMatchSnapshot();
       expect(res.deps).toHaveLength(2);
     });
+
     it('should handle invalid YAML', () => {
       const res = extractPackageFile(invalidYAML);
       expect(res).toBeNull();
     });
+
+    it('handles matrix node_js syntax with node_js string', () => {
+      const res = extractPackageFile(matrixYAMLwithNodeSyntaxString);
+      expect(res).toEqual({
+        deps: [
+          {
+            currentValue: '11.10.1',
+            datasource: 'github-tags',
+            depName: 'node',
+            lookupName: 'nodejs/node',
+          },
+        ],
+      });
+    });
+
+    it('handles matrix node_js syntax with node_js array', () => {
+      const res = extractPackageFile(matrixYAMLwithNodeSyntaxArray);
+      expect(res).toEqual({
+        deps: [
+          {
+            currentValue: '11.10.1',
+            datasource: 'github-tags',
+            depName: 'node',
+            lookupName: 'nodejs/node',
+          },
+          {
+            currentValue: '11.10.2',
+            datasource: 'github-tags',
+            depName: 'node',
+            lookupName: 'nodejs/node',
+          },
+        ],
+      });
+    });
+
+    it('handles matrix node_js syntax with node_js array 2', () => {
+      const res = extractPackageFile(matrixYAMLwithNodeSyntaxArray2);
+      expect(res).toEqual({
+        deps: [
+          {
+            currentValue: '11.10.1',
+            datasource: 'github-tags',
+            depName: 'node',
+            lookupName: 'nodejs/node',
+          },
+          {
+            currentValue: '11.10.2',
+            datasource: 'github-tags',
+            depName: 'node',
+            lookupName: 'nodejs/node',
+          },
+        ],
+      });
+    });
+
+    it('handles matrix node_js syntax with alias', () => {
+      const res = extractPackageFile(matrixYAMLwithNodeSyntaxAlias);
+      expect(res).toEqual({
+        deps: [
+          {
+            currentValue: '11.10.1',
+            datasource: 'github-tags',
+            depName: 'node',
+            lookupName: 'nodejs/node',
+          },
+        ],
+      });
+    });
+
+    it('handles invalid matrix node_js syntax', () => {
+      const res = extractPackageFile(invalidMatrixYAML);
+      expect(res).toBeNull();
+    });
   });
 });
diff --git a/lib/manager/travis/extract.ts b/lib/manager/travis/extract.ts
index 58ffe64021..c50ad28bf0 100644
--- a/lib/manager/travis/extract.ts
+++ b/lib/manager/travis/extract.ts
@@ -3,12 +3,14 @@ import { load } from 'js-yaml';
 import * as datasourceGithubTags from '../../datasource/github-tags';
 import { logger } from '../../logger';
 import type { PackageDependency, PackageFile } from '../types';
+import type { TravisMatrixItem, TravisYaml } from './types';
 
 export function extractPackageFile(content: string): PackageFile | null {
-  // TODO: fix type
-  let doc: any;
+  let doc: TravisYaml | null;
   try {
-    doc = load(content, { json: true });
+    doc = load(content, {
+      json: true,
+    });
   } catch (err) {
     logger.warn({ err, content }, 'Failed to parse .travis.yml file.');
     return null;
@@ -22,6 +24,41 @@ export function extractPackageFile(content: string): PackageFile | null {
       currentValue: currentValue.toString(),
     }));
   }
+
+  // Handle the matrix syntax
+  let matrix_include: TravisMatrixItem[] | undefined;
+  if (doc?.jobs?.include) {
+    matrix_include = doc.jobs.include;
+  } else if (doc?.matrix?.include) {
+    matrix_include = doc.matrix.include;
+  }
+
+  if (!is.array(matrix_include)) {
+    return deps.length ? { deps } : null;
+  }
+
+  for (const item of matrix_include) {
+    if (item?.node_js) {
+      if (is.array(item.node_js)) {
+        item.node_js.forEach((currentValue) => {
+          deps.push({
+            depName: 'node',
+            datasource: datasourceGithubTags.id,
+            lookupName: 'nodejs/node',
+            currentValue: currentValue.toString(),
+          });
+        });
+      } else if (is.string(item.node_js)) {
+        deps.push({
+          depName: 'node',
+          datasource: datasourceGithubTags.id,
+          lookupName: 'nodejs/node',
+          currentValue: item.node_js.toString(),
+        });
+      }
+    }
+  }
+
   if (!deps.length) {
     return null;
   }
diff --git a/lib/manager/travis/readme.md b/lib/manager/travis/readme.md
index fb728f0b74..6e5cbaed90 100644
--- a/lib/manager/travis/readme.md
+++ b/lib/manager/travis/readme.md
@@ -1,14 +1,14 @@
 This manager is intended to keep Travis config files (`.travis.yml`) up-to-date, this file controls the CI build environment.
 Currently Renovate can only update the `node_js` section of this file.
 
-An important limitation is that Renovate does not currently "understand" [Travis's Build Matrix concept](https://docs.travis-ci.com/user/build-matrix/#matrix-expansion), so it will try to update all found Node.js versions to the latest LTS, e.g.
+Renovate "understands" [Travis's Build Matrix concept](https://docs.travis-ci.com/user/build-matrix/#matrix-expansion) as well, so it will try to update all found Node.js versions to the latest LTS, e.g.
 
 ```diff
 node_js:
 -  - 8.10.0
 -  - 10.10.0
-+  - 14.17.4
-+  - 14.17.4
++  - 16.13.0
++  - 16.13.0
 ```
 
 Due to this, major updates for Travis are disabled by default.
@@ -24,5 +24,3 @@ Here's how to enable major updates in your Renovate config:
   }
 }
 ```
-
-If you would like to see "build matrix" support in future, please contribute ideas to [issue #11175](https://github.com/renovatebot/renovate/issues/11175).
diff --git a/lib/manager/travis/types.ts b/lib/manager/travis/types.ts
new file mode 100644
index 0000000000..937eb4ded0
--- /dev/null
+++ b/lib/manager/travis/types.ts
@@ -0,0 +1,19 @@
+// travis.yml syntax description:
+//  - regular: https://docs.travis-ci.com/user/tutorial/
+//  - matrix: https://docs.travis-ci.com/user/build-matrix/
+
+export type TravisNodeJs = string | string[];
+
+export interface TravisYaml {
+  node_js?: TravisNodeJs;
+  jobs?: TravisMatrix;
+  matrix?: TravisMatrix;
+}
+
+export interface TravisMatrixItem {
+  node_js?: TravisNodeJs;
+}
+
+export interface TravisMatrix {
+  include?: TravisMatrixItem[];
+}
-- 
GitLab