From 7a818935c0c89a5e90f573cd151f7aa008e494f2 Mon Sep 17 00:00:00 2001
From: Thach Nguyen <thachnn80@gmail.com>
Date: Fri, 29 Jul 2022 04:37:38 +0700
Subject: [PATCH] Implement feature versionSpec using SemVer for setup Maven

---
 .gitignore                  |   1 +
 README.md                   |   4 +-
 __tests__/installer.test.ts |  11 +-
 action.yml                  |   5 +-
 dist/index.js               | 197 +++++++++++++++++++++++++++++++-----
 package.json                |   3 +-
 src/installer.ts            |  68 +++++++++++--
 src/main.ts                 |  26 +++++
 src/setup-maven.ts          |  15 +--
 src/utils.ts                |  47 +++++++++
 10 files changed, 319 insertions(+), 58 deletions(-)
 create mode 100644 src/main.ts
 create mode 100644 src/utils.ts

diff --git a/.gitignore b/.gitignore
index 283c045..97de0bb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,6 +16,7 @@ lerna-debug.log*
 
 # Diagnostic reports (https://nodejs.org/api/report.html)
 report.*.*.*.*.json
+.scannerwork/
 
 # Runtime data
 pids
diff --git a/README.md b/README.md
index 7bb90f9..1d16d98 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@ jobs:
     - name: Set up Maven
       uses: stCarolas/setup-maven@v5
       with:
-        maven-version: 3.8.2
+        maven-version: 3.8
 ```
 
 ### Development using [Docker](https://docs.docker.com/)
@@ -41,5 +41,5 @@ Run `SonarScanner` from [the Docker image](https://hub.docker.com/r/sonarsource/
 ```batch
 docker run --rm -it --link docker-sonarqube -v "%PWD%:/usr/src/app" -w /usr/src/app ^
   -e "SONAR_HOST_URL=http://docker-sonarqube:9000" -e "SONAR_LOGIN=<projectToken>" sonarsource/sonar-scanner-cli ^
-  -Dsonar.projectKey=setup-maven -Dsonar.language=js -Dsonar.sources=. "-Dsonar.exclusions=dist/**"
+  -Dsonar.projectKey=setup-maven -Dsonar.sources=. "-Dsonar.exclusions=dist/**,lib/**"
 ```
diff --git a/__tests__/installer.test.ts b/__tests__/installer.test.ts
index 7363f55..1d70ec2 100644
--- a/__tests__/installer.test.ts
+++ b/__tests__/installer.test.ts
@@ -1,5 +1,10 @@
-describe('maven installer tests', () => {
-  it('square root of 4 to equal 2', () => {
-    expect(Math.sqrt(4)).toBe(2);
+import * as installer from '../src/installer';
+
+describe('getAvailableVersions', () => {
+  it('load real available versions', async () => {
+    const availableVersions = await installer.getAvailableVersions();
+
+    expect(availableVersions).toBeTruthy();
+    expect(availableVersions).toEqual(expect.arrayContaining(['3.2.5', '3.3.3', '3.8.2']));
   });
 });
diff --git a/action.yml b/action.yml
index 5e8dcaf..dd784e9 100644
--- a/action.yml
+++ b/action.yml
@@ -5,7 +5,10 @@ inputs:
   maven-version:
     description: 'Version Spec of the version to use. Examples: 3.x, 3.1.1, >=3.8.0'
     required: false
-    default: '3.8.2'
+    default: '3'
+outputs:
+  version:
+    description: 'Actual version of Apache Maven that has been installed'
 runs:
   using: 'node16'
   main: 'dist/index.js'
diff --git a/dist/index.js b/dist/index.js
index 2f8ee13..8f45dd4 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -4965,37 +4965,81 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
     });
 };
 Object.defineProperty(exports, "__esModule", ({ value: true }));
-exports.getMaven = void 0;
+exports.findVersionForDownload = exports.downloadMaven = exports.getAvailableVersions = exports.setupMaven = void 0;
+const path = __importStar(__nccwpck_require__(17));
 const core = __importStar(__nccwpck_require__(186));
 const tc = __importStar(__nccwpck_require__(784));
-const path = __importStar(__nccwpck_require__(17));
-function getMaven(version) {
+const http_client_1 = __nccwpck_require__(925);
+const semver = __importStar(__nccwpck_require__(911));
+const utils_1 = __nccwpck_require__(314);
+function setupMaven(versionSpec, installedVersion) {
     return __awaiter(this, void 0, void 0, function* () {
-        let toolPath = tc.find('maven', version);
-        if (!toolPath) {
-            toolPath = yield downloadMaven(version);
+        let toolPath = tc.find('maven', versionSpec);
+        let resolvedVersion = utils_1.getVersionFromToolcachePath(toolPath);
+        if (installedVersion) {
+            if (!toolPath || semver.gte(installedVersion, resolvedVersion)) {
+                core.info(`Use system Maven version ${installedVersion} instead of the cached one: ${resolvedVersion}`);
+                return installedVersion;
+            }
+        }
+        else if (!toolPath) {
+            resolvedVersion = yield findVersionForDownload(versionSpec);
+            toolPath = yield downloadMaven(resolvedVersion);
         }
         core.addPath(path.join(toolPath, 'bin'));
+        return resolvedVersion;
     });
 }
-exports.getMaven = getMaven;
+exports.setupMaven = setupMaven;
 const DOWNLOAD_BASE_URL = 'https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven';
-function downloadMaven(version) {
+function getAvailableVersions() {
     return __awaiter(this, void 0, void 0, function* () {
-        const toolDirectoryName = `apache-maven-${version}`;
-        const downloadUrl = `${DOWNLOAD_BASE_URL}/${version}/${toolDirectoryName}-bin.tar.gz`;
-        core.info(`Downloading Maven ${version} from ${downloadUrl} ...`);
+        const resourceUrl = `${DOWNLOAD_BASE_URL}/maven-metadata.xml`;
+        const http = new http_client_1.HttpClient('setup-maven', undefined, { allowRetries: true });
+        core.info(`Downloading Maven versions manifest from ${resourceUrl} ...`);
+        const response = yield http.get(resourceUrl);
+        const body = yield response.readBody();
+        if (response.message.statusCode !== http_client_1.HttpCodes.OK || !body) {
+            throw new Error(`Unable to get available versions from ${resourceUrl}`);
+        }
+        const availableVersions = body.match(/(?<=<version>)[^<>]+(?=<\/version>)/g) || [];
+        core.debug(`Available Maven versions: [${availableVersions}]`);
+        return availableVersions;
+    });
+}
+exports.getAvailableVersions = getAvailableVersions;
+/**
+ * Download and extract a specified Maven version to the tool-cache.
+ */
+function downloadMaven(fullVersion) {
+    return __awaiter(this, void 0, void 0, function* () {
+        const toolDirectoryName = `apache-maven-${fullVersion}`;
+        const downloadUrl = `${DOWNLOAD_BASE_URL}/${fullVersion}/${toolDirectoryName}-bin.tar.gz`;
+        core.info(`Downloading Maven ${fullVersion} from ${downloadUrl} ...`);
         const downloadPath = yield tc.downloadTool(downloadUrl);
         const extractedPath = yield tc.extractTar(downloadPath);
         const toolRoot = path.join(extractedPath, toolDirectoryName);
-        return tc.cacheDir(toolRoot, 'maven', version);
+        return tc.cacheDir(toolRoot, 'maven', fullVersion);
     });
 }
+exports.downloadMaven = downloadMaven;
+function findVersionForDownload(versionSpec) {
+    return __awaiter(this, void 0, void 0, function* () {
+        const availableVersions = yield getAvailableVersions();
+        const resolvedVersion = semver.maxSatisfying(availableVersions, versionSpec);
+        if (!resolvedVersion) {
+            throw new Error(`Could not find satisfied version for SemVer ${versionSpec}`);
+        }
+        core.debug(`Resolved version for download: ${resolvedVersion}`);
+        return resolvedVersion;
+    });
+}
+exports.findVersionForDownload = findVersionForDownload;
 
 
 /***/ }),
 
-/***/ 587:
+/***/ 399:
 /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
 
 "use strict";
@@ -5029,23 +5073,115 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
     });
 };
 Object.defineProperty(exports, "__esModule", ({ value: true }));
+exports.run = void 0;
 const core = __importStar(__nccwpck_require__(186));
-const installer = __importStar(__nccwpck_require__(574));
+const semver = __importStar(__nccwpck_require__(911));
+const utils_1 = __nccwpck_require__(314);
+const installer_1 = __nccwpck_require__(574);
 function run() {
     return __awaiter(this, void 0, void 0, function* () {
         try {
-            const version = core.getInput('maven-version');
-            if (version) {
-                yield installer.getMaven(version);
+            const versionSpec = core.getInput('maven-version') || '3';
+            if (!semver.validRange(versionSpec)) {
+                core.setFailed(`Invalid SemVer notation '${versionSpec}' for a Maven version`);
+                return;
             }
+            let installedVersion = yield utils_1.getActiveMavenVersion();
+            if (installedVersion && !semver.satisfies(installedVersion, versionSpec)) {
+                installedVersion = undefined;
+            }
+            installedVersion = yield installer_1.setupMaven(versionSpec, installedVersion);
+            core.setOutput('version', installedVersion);
         }
         catch (error) {
-            core.setFailed(error.message);
+            core.setFailed(error.toString());
         }
     });
 }
-// noinspection JSIgnoredPromiseFromCall
-run();
+exports.run = run;
+
+
+/***/ }),
+
+/***/ 314:
+/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
+
+"use strict";
+
+var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
+    if (k2 === undefined) k2 = k;
+    Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
+}) : (function(o, m, k, k2) {
+    if (k2 === undefined) k2 = k;
+    o[k2] = m[k];
+}));
+var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
+    Object.defineProperty(o, "default", { enumerable: true, value: v });
+}) : function(o, v) {
+    o["default"] = v;
+});
+var __importStar = (this && this.__importStar) || function (mod) {
+    if (mod && mod.__esModule) return mod;
+    var result = {};
+    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
+    __setModuleDefault(result, mod);
+    return result;
+};
+var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
+    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
+    return new (P || (P = Promise))(function (resolve, reject) {
+        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
+        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
+        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
+        step((generator = generator.apply(thisArg, _arguments || [])).next());
+    });
+};
+Object.defineProperty(exports, "__esModule", ({ value: true }));
+exports.getExecOutput = exports.getActiveMavenVersion = exports.getVersionFromToolcachePath = void 0;
+const path = __importStar(__nccwpck_require__(17));
+const core = __importStar(__nccwpck_require__(186));
+const exec = __importStar(__nccwpck_require__(514));
+function getVersionFromToolcachePath(toolPath) {
+    return !toolPath ? toolPath : path.basename(path.dirname(toolPath));
+}
+exports.getVersionFromToolcachePath = getVersionFromToolcachePath;
+/**
+ * Determine version of the current used Maven.
+ */
+function getActiveMavenVersion() {
+    return __awaiter(this, void 0, void 0, function* () {
+        try {
+            const { output } = yield getExecOutput('mvn', ['-v']);
+            const found = output.match(/^[^\d]*(\S+)/);
+            const installedVersion = !found ? '' : found[1];
+            core.debug(`Retrieved activated Maven version: ${installedVersion}`);
+            return installedVersion;
+        }
+        catch (error) {
+            core.info(`Failed to get activated Maven version. ${error}`);
+        }
+        return undefined;
+    });
+}
+exports.getActiveMavenVersion = getActiveMavenVersion;
+/**
+ * Exec a command and get the standard output.
+ *
+ * @throws {Error} If the exit-code is non-zero.
+ */
+function getExecOutput(command, args) {
+    return __awaiter(this, void 0, void 0, function* () {
+        let output = '';
+        const exitCode = yield exec.exec(command, args, {
+            silent: true,
+            listeners: {
+                stdout: (data) => (output += data.toString())
+            }
+        });
+        return { exitCode, output };
+    });
+}
+exports.getExecOutput = getExecOutput;
 
 
 /***/ }),
@@ -5192,12 +5328,19 @@ module.exports = require("util");
 /******/ 	if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = __dirname + "/";
 /******/ 	
 /************************************************************************/
-/******/ 	
-/******/ 	// startup
-/******/ 	// Load entry module and return exports
-/******/ 	// This entry module is referenced by other modules so it can't be inlined
-/******/ 	var __webpack_exports__ = __nccwpck_require__(587);
-/******/ 	module.exports = __webpack_exports__;
-/******/ 	
+var __webpack_exports__ = {};
+// This entry need to be wrapped in an IIFE because it need to be in strict mode.
+(() => {
+"use strict";
+var exports = __webpack_exports__;
+
+Object.defineProperty(exports, "__esModule", ({ value: true }));
+const main_1 = __nccwpck_require__(399);
+// noinspection JSIgnoredPromiseFromCall
+main_1.run();
+
+})();
+
+module.exports = __webpack_exports__;
 /******/ })()
 ;
\ No newline at end of file
diff --git a/package.json b/package.json
index 6791424..5e3bce2 100644
--- a/package.json
+++ b/package.json
@@ -38,9 +38,10 @@
     "typescript": "^4.2.3"
   },
   "prettier": {
+    "printWidth": 100,
     "semi": true,
+    "singleQuote": true,
     "trailingComma": "none",
-    "bracketSpacing": true,
     "arrowParens": "avoid"
   },
   "jest": {
diff --git a/src/installer.ts b/src/installer.ts
index 76bb96c..a90c380 100644
--- a/src/installer.ts
+++ b/src/installer.ts
@@ -1,29 +1,77 @@
+import * as path from 'path';
 import * as core from '@actions/core';
 import * as tc from '@actions/tool-cache';
+import { HttpClient, HttpCodes } from '@actions/http-client';
+import * as semver from 'semver';
 
-import * as path from 'path';
+import { getVersionFromToolcachePath } from './utils';
 
-export async function getMaven(version: string) {
-  let toolPath = tc.find('maven', version);
+export async function setupMaven(versionSpec: string, installedVersion?: string): Promise<string> {
+  let toolPath = tc.find('maven', versionSpec);
+  let resolvedVersion = getVersionFromToolcachePath(toolPath);
 
-  if (!toolPath) {
-    toolPath = await downloadMaven(version);
+  if (installedVersion) {
+    if (!toolPath || semver.gte(installedVersion, resolvedVersion)) {
+      core.info(
+        `Use system Maven version ${installedVersion} instead of the cached one: ${resolvedVersion}`
+      );
+
+      return installedVersion;
+    }
+  } else if (!toolPath) {
+    resolvedVersion = await findVersionForDownload(versionSpec);
+
+    toolPath = await downloadMaven(resolvedVersion);
   }
 
   core.addPath(path.join(toolPath, 'bin'));
+  return resolvedVersion;
 }
 
 const DOWNLOAD_BASE_URL = 'https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven';
 
-async function downloadMaven(version: string): Promise<string> {
-  const toolDirectoryName = `apache-maven-${version}`;
-  const downloadUrl = `${DOWNLOAD_BASE_URL}/${version}/${toolDirectoryName}-bin.tar.gz`;
+export async function getAvailableVersions(): Promise<string[]> {
+  const resourceUrl = `${DOWNLOAD_BASE_URL}/maven-metadata.xml`;
+  const http = new HttpClient('setup-maven', undefined, { allowRetries: true });
 
-  core.info(`Downloading Maven ${version} from ${downloadUrl} ...`);
+  core.info(`Downloading Maven versions manifest from ${resourceUrl} ...`);
+  const response = await http.get(resourceUrl);
+  const body = await response.readBody();
+
+  if (response.message.statusCode !== HttpCodes.OK || !body) {
+    throw new Error(`Unable to get available versions from ${resourceUrl}`);
+  }
+
+  const availableVersions = body.match(/(?<=<version>)[^<>]+(?=<\/version>)/g) || [];
+  core.debug(`Available Maven versions: [${availableVersions}]`);
+
+  return availableVersions;
+}
+
+/**
+ * Download and extract a specified Maven version to the tool-cache.
+ */
+export async function downloadMaven(fullVersion: string): Promise<string> {
+  const toolDirectoryName = `apache-maven-${fullVersion}`;
+  const downloadUrl = `${DOWNLOAD_BASE_URL}/${fullVersion}/${toolDirectoryName}-bin.tar.gz`;
+
+  core.info(`Downloading Maven ${fullVersion} from ${downloadUrl} ...`);
   const downloadPath = await tc.downloadTool(downloadUrl);
 
   const extractedPath = await tc.extractTar(downloadPath);
 
   const toolRoot = path.join(extractedPath, toolDirectoryName);
-  return tc.cacheDir(toolRoot, 'maven', version);
+  return tc.cacheDir(toolRoot, 'maven', fullVersion);
+}
+
+export async function findVersionForDownload(versionSpec: string): Promise<string> {
+  const availableVersions = await getAvailableVersions();
+
+  const resolvedVersion = semver.maxSatisfying(availableVersions, versionSpec);
+  if (!resolvedVersion) {
+    throw new Error(`Could not find satisfied version for SemVer ${versionSpec}`);
+  }
+
+  core.debug(`Resolved version for download: ${resolvedVersion}`);
+  return resolvedVersion;
 }
diff --git a/src/main.ts b/src/main.ts
new file mode 100644
index 0000000..5249e65
--- /dev/null
+++ b/src/main.ts
@@ -0,0 +1,26 @@
+import * as core from '@actions/core';
+import * as semver from 'semver';
+
+import { getActiveMavenVersion } from './utils';
+import { setupMaven } from './installer';
+
+export async function run() {
+  try {
+    const versionSpec = core.getInput('maven-version') || '3';
+
+    if (!semver.validRange(versionSpec)) {
+      core.setFailed(`Invalid SemVer notation '${versionSpec}' for a Maven version`);
+      return;
+    }
+
+    let installedVersion = await getActiveMavenVersion();
+    if (installedVersion && !semver.satisfies(installedVersion, versionSpec)) {
+      installedVersion = undefined;
+    }
+
+    installedVersion = await setupMaven(versionSpec, installedVersion);
+    core.setOutput('version', installedVersion);
+  } catch (error) {
+    core.setFailed(error.toString());
+  }
+}
diff --git a/src/setup-maven.ts b/src/setup-maven.ts
index 6eca19a..59534a3 100644
--- a/src/setup-maven.ts
+++ b/src/setup-maven.ts
@@ -1,17 +1,4 @@
-import * as core from '@actions/core';
-
-import * as installer from './installer';
-
-async function run() {
-  try {
-    const version = core.getInput('maven-version');
-    if (version) {
-      await installer.getMaven(version);
-    }
-  } catch (error) {
-    core.setFailed(error.message);
-  }
-}
+import { run } from './main';
 
 // noinspection JSIgnoredPromiseFromCall
 run();
diff --git a/src/utils.ts b/src/utils.ts
new file mode 100644
index 0000000..36571d4
--- /dev/null
+++ b/src/utils.ts
@@ -0,0 +1,47 @@
+import * as path from 'path';
+import * as core from '@actions/core';
+import * as exec from '@actions/exec';
+
+export function getVersionFromToolcachePath(toolPath: string) {
+  return !toolPath ? toolPath : path.basename(path.dirname(toolPath));
+}
+
+/**
+ * Determine version of the current used Maven.
+ */
+export async function getActiveMavenVersion(): Promise<string | undefined> {
+  try {
+    const { output } = await getExecOutput('mvn', ['-v']);
+
+    const found = output.match(/^[^\d]*(\S+)/);
+    const installedVersion = !found ? '' : found[1];
+    core.debug(`Retrieved activated Maven version: ${installedVersion}`);
+
+    return installedVersion;
+  } catch (error) {
+    core.info(`Failed to get activated Maven version. ${error}`);
+  }
+
+  return undefined;
+}
+
+/**
+ * Exec a command and get the standard output.
+ *
+ * @throws {Error} If the exit-code is non-zero.
+ */
+export async function getExecOutput(
+  command: string,
+  args?: string[]
+): Promise<{ exitCode: number; output: string }> {
+  let output = '';
+
+  const exitCode = await exec.exec(command, args, {
+    silent: true,
+    listeners: {
+      stdout: (data: Buffer) => (output += data.toString())
+    }
+  });
+
+  return { exitCode, output };
+}