Repository: FHIR/sushi (ts)

Issue:
Support flag to require latest version of SUSHI
During the FHIR IG publication workflow, it is very important that the latest version of SUSHI be used. To support this, SUSHI should introduce an optional flag that indicates that SUSHI should exit with an error if it isn't the latest version (or if it can't determine the latest version).  The default value should be false, but FHIR publication engineers can set the flag when doing a publication.

See: https://chat.fhir.org/#narrow/stream/215610-shorthand/topic/FSH.20install.20problem/near/281720829
SUSHI should only warn when it cannot determine latest version (unless --require-latest flag is set)
Currently, SUSHI issues an error if it can't reach NPM to determine if it is running the latest version.  This is likely too strong, especially for authors who may be intentionally running a different version of SUSHI.  This should be demoted to be a warning.

If SUSHI is run w/ the `--require-latest` flag, however, SUSHI should _still_ exit w/ an error.

Pull request:
Support for --require-latest
This PR contains two changes to how SUSHI handles version checking:
* If SUSHI cannot determine the latest version (failure to contact NPM), issue a _warning_ instead of an _error_.  Also add a message to the summary in this case so that it's easier to see that there was an issue.
* If SUSHI is run w/ the `--require-latest` flag then exit with an error if SUSHI is not the latest (or cannot determine the latest).

Some of this has modified unit tests, but the code in `app.ts` does not have unit tests. To manually test this:

First:
* Run SUSHI(`ts-node src/app.ts path-to-project`) and confirm it runs successfully w/ no errors or warnings about the SUSHI version.
* Run SUSHI w/ the `--require-latest` flag and confirm that SUSHI still runs successfully.

Then:
* Change the `package.json` in this branch to say `2.4.0` so it appears to be out of date.
* Run SUSHI and confirm there are no warnings/errors, but there _is_ a message in the box indicating that SUSHI is out of date.
* Run SUSHI with the `--require-latest` flag and confirm that an error is logged and SUSHI exits.

Finally:
* Turn off wifi and unplug network cables so you have no internet.
* Run SUSHI and confirm that there is a warning that it could not detect the latest version (individual log as well as summary at end).
* Run SUSHI with the `--require-latest` flag and confirm that the warning _and_ an error is logged and SUSHI exits.
* Don't forget to turn back on wifi and/or plug back in network cables.

Fixes #1083 
Fixes #1084 

Patch:
diff --git a/README.md b/README.md
index dbf905b..f3cd857 100644
--- a/README.md
+++ b/README.md
@@ -23,21 +23,21 @@ After installation, the `sushi` commandline will be available on your path:
 
 ```sh
 $ sushi --help
-Usage: sushi [path-to-fsh-defs] [options]
+Usage: sushi [path-to-fsh-project] [options]
 
 Options:
-  -o, --out <out>     the path to the output folder
-  -d, --debug         output extra debugging information
-  -p, --preprocessed  output FSH produced by preprocessing steps
-  -s, --snapshot      generate snapshot in Structure Definition output (default: false)
-  -i, --init          initialize a SUSHI project
-  -v, --version       print SUSHI version
-  -h, --help          output usage information
+  -o, --out <out>       the path to the output folder
+  -d, --debug           output extra debugging information
+  -p, --preprocessed    output FSH produced by preprocessing steps
+  -s, --snapshot        generate snapshot in Structure Definition output (default: false)
+  -r, --require-latest  exit with error if this is not the latest version of SUSHI (default: false)
+  -i, --init            initialize a SUSHI project
+  -v, --version         print SUSHI version
+  -h, --help            output usage information
 
 Additional information:
-  [path-to-fsh-defs]
+  [path-to-fsh-project]
     Default: "."
-    If input/fsh/ subdirectory present, it is included in [path-to-fsh-defs]
   -o, --out <out>
     Default: "fsh-generated"
 
diff --git a/src/app.ts b/src/app.ts
index d9c17b0..e790bea 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -50,6 +50,11 @@ async function app() {
     .option('-d, --debug', 'output extra debugging information')
     .option('-p, --preprocessed', 'output FSH produced by preprocessing steps')
     .option('-s, --snapshot', 'generate snapshot in Structure Definition output', false)
+    .option(
+      '-r, --require-latest',
+      'exit with error if this is not the latest version of SUSHI',
+      false
+    )
     .option('-i, --init', 'initialize a SUSHI project')
     .version(getVersion(), '-v, --version', 'print SUSHI version')
     .on('--help', () => {
@@ -71,6 +76,41 @@ async function app() {
     process.exit(0);
   }
   if (program.debug) logger.level = 'debug';
+
+  logger.info(`Running ${getVersion()}`);
+  logger.info('Arguments:');
+  if (program.debug) {
+    logger.info('  --debug');
+  }
+  if (program.preprocessed) {
+    logger.info('  --preprocessed');
+  }
+  if (program.snapshot) {
+    logger.info('  --snapshot');
+  }
+  if (program.requireLatest) {
+    logger.info('  --require-latest');
+  }
+  if (program.out) {
+    logger.info(`  --out ${path.resolve(program.out)}`);
+  }
+  logger.info(`  ${path.resolve(input)}`);
+
+  const sushiVersions = await checkSushiVersion();
+  if (
+    program.requireLatest &&
+    (sushiVersions.latest == null || sushiVersions.latest !== sushiVersions.current)
+  ) {
+    logger.error(
+      `Current SUSHI version (${
+        sushiVersions.current
+      }) is not the latest version. Upgrade to the latest version (${
+        sushiVersions.latest ?? 'undetermined'
+      }) or run SUSHI again without the --require-latest flag.`
+    );
+    process.exit(1);
+  }
+
   input = ensureInputDir(input);
 
   const rootIgnoreWarningsPath = path.join(input, 'sushi-ignoreWarnings.txt');
@@ -90,23 +130,6 @@ async function app() {
     setIgnoredWarnings(fs.readFileSync(nestedIgnoreWarningsPath, 'utf-8'));
   }
 
-  logger.info(`Running ${getVersion()}`);
-
-  logger.info('Arguments:');
-  if (program.debug) {
-    logger.info('  --debug');
-  }
-  if (program.preprocessed) {
-    logger.info('  --preprocessed');
-  }
-  if (program.snapshot) {
-    logger.info('  --snapshot');
-  }
-  if (program.out) {
-    logger.info(`  --out ${path.resolve(program.out)}`);
-  }
-  logger.info(`  ${path.resolve(input)}`);
-
   const originalInput = input;
   input = findInputDir(input);
 
@@ -197,7 +220,6 @@ async function app() {
   }
 
   console.log();
-  const sushiVersions = await checkSushiVersion();
   printResults(outPackage, sushiVersions);
 
   console.log();
@@ -214,7 +236,6 @@ function getVersion(): string {
 }
 
 function printResults(pkg: Package, sushiVersions: any) {
-  const { latest, current } = sushiVersions;
   // NOTE: These variables are creatively names to align well in the strings below while keeping prettier happy
   const profileNum = pad(pkg.profiles.length.toString(), 13);
   const extentNum = pad(pkg.extensions.length.toString(), 12);
@@ -249,14 +270,25 @@ function printResults(pkg: Package, sushiVersions: any) {
     clr('╚' + '═════════════════════════════════════════════════════════════════' + '' + '╝')
   ];
 
-  if (latest != null && current !== 'unknown' && latest !== current) {
+  const { latest, current } = sushiVersions;
+  if (latest != null && current != null && latest !== current) {
+    const endline = results.pop();
+    // prettier-ignore
+    results.push(
+      clr('╠'  +     '═════════════════════════════════════════════════════════════════'      +     '╣'),
+      clr('║') +   pad(`You are using SUSHI version ${current}, but the latest stable`, 65)   + clr('║'),
+      clr('║') + pad(`release is version ${latest}. To install the latest release, run:`, 65) + clr('║'),
+      clr('║') +                  pad('npm install -g fsh-sushi',65)                          + clr('║'),
+      endline
+    );
+  } else if (latest == null || current == null) {
     const endline = results.pop();
     // prettier-ignore
     results.push(
-      clr('╠'  + '═════════════════════════════════════════════════════════════════' + '' + '╣'),
-      clr('║') + `    You are using SUSHI version ${current}, but the latest stable     ` + '' + clr('║'),
-      clr('║') + `  release is version ${latest}. To install the latest release, run:  ` + '' + clr('║'),
-      clr('║') + '                  npm install -g fsh-sushi                       ' + '' + clr('║'),
+      clr('╠'  + '═════════════════════════════════════════════════════════════════'    +      '╣'),
+      clr('║') + pad('SUSHI cannot determine if it is running the latest version.', 65) +  clr('║'),
+      clr('║') + pad('To see a listing of releases, including the latest, visit:', 65)  +  clr('║'),
+      clr('║') +          pad('https://github.com/FHIR/sushi/releases', 65)             +  clr('║'),
       endline
     );
   }
diff --git a/src/utils/Processing.ts b/src/utils/Processing.ts
index 8b8afad..7fe8caa 100644
--- a/src/utils/Processing.ts
+++ b/src/utils/Processing.ts
@@ -563,12 +563,12 @@ export async function getLatestSushiVersion(): Promise<string> {
     const res = await axiosGet('https://registry.npmjs.org/fsh-sushi');
     const latestVer = res.data['dist-tags'].latest;
     if (latestVer == null) {
-      logger.error('Unable to determine the latest version of sushi.');
+      logger.warn('Unable to determine the latest version of sushi.');
     } else {
       return latestVer;
     }
   } catch (e) {
-    logger.error(`Unable to determine the latest version of sushi: ${e.message}`);
+    logger.warn(`Unable to determine the latest version of sushi: ${e.message}`);
   }
 }
 


Test patch:
diff --git a/test/utils/Processing.test.ts b/test/utils/Processing.test.ts
index 9b88c80..a8faeed 100644
--- a/test/utils/Processing.test.ts
+++ b/test/utils/Processing.test.ts
@@ -1319,7 +1319,7 @@ describe('Processing', () => {
       };
       mockedAxios.get.mockImplementationOnce(() => Promise.resolve(data));
       await getLatestSushiVersion();
-      expect(loggerSpy.getLastMessage('error')).toMatch(
+      expect(loggerSpy.getLastMessage('warn')).toMatch(
         'Unable to determine the latest version of sushi.'
       );
     });
@@ -1328,7 +1328,7 @@ describe('Processing', () => {
       const data = {};
       mockedAxios.get.mockImplementationOnce(() => Promise.resolve(data));
       await getLatestSushiVersion();
-      expect(loggerSpy.getLastMessage('error')).toMatch(
+      expect(loggerSpy.getLastMessage('warn')).toMatch(
         "Unable to determine the latest version of sushi: Cannot read property 'dist-tags' of undefined"
       );
     });