Repository: FHIR/sushi
Pull request #610 (2020-09-23 17:46:19)

Revert "Remove generation of package-list from history (#606)"
This reverts commit 6b17523161284176531f192f7bb1ac2275f82997.

To provide a smooth transition, we should deprecate package-list.json w/ warnings before outright removing it...


Patch:
diff --git a/README.md b/README.md
index a0bd8c0..2eccd4a 100644
--- a/README.md
+++ b/README.md
@@ -49,7 +49,7 @@ SUSHI supports publishing implementation guides via the new template-based IG Pu
 If the input folder (i.e., "FSH Tank") contains a sub-folder named "ig-data", then SUSHI will generate a basic Implementation Guide project that can be built using the template-based IG Publisher.  SUSHI currently supports limited customization of the IG via the following files:
 
 * `ig-data/ig.ini`: If present, the user-provided igi.ini values will be merged with SUSHI-generated ig.ini.
-* `ig-data/package-list.json`: If present, will be copied into the corresponding location in the IG input folder.
+* `ig-data/package-list.json`: If present, it will be used instead of a generated package-list.json.
 * `ig-data/input/ignoreWarnings.txt`: If present, will be copied into the corresponding location in the IG input folder
 * `ig-data/input/images/*`: If present, image files will be copied into the IG input and can be referenced by user-provided pages.
 * `ig-data/input/pagecontent/index.[md|xml]`: If present, it will provide the content for the IG's main page.
diff --git a/src/fshtypes/Configuration.ts b/src/fshtypes/Configuration.ts
index 7c974d6..91a4848 100644
--- a/src/fshtypes/Configuration.ts
+++ b/src/fshtypes/Configuration.ts
@@ -23,6 +23,7 @@ import {
  * @see {@link http://hl7.org/fhir/R4/implementationguide.html}
  * @see {@link https://build.fhir.org/ig/FHIR/ig-guidance/using-templates.html#igroot}
  * @see {@link https://confluence.hl7.org/display/FHIR/NPM+Package+Specification}
+ * @see {@link https://confluence.hl7.org/pages/viewpage.action?pageId=66928420#FHIRIGPackageListdoco-PublicationObject}
  * @see {@link https://github.com/FHIR/sample-ig/blob/master/input/includes/menu.xml}
  */
 export type Configuration = {
@@ -113,6 +114,21 @@ export type Configuration = {
   // a menu)?
   menu?: ConfigurationMenuItem[];
 
+  // The history property corresponds to package-list.json. SUSHI will use the existing top-level
+  // properties in its config to populate the top-level package-list.json properties: package-id,
+  // canonical, title, and introduction. Authors that wish to provide different values can supply
+  // them as properties under history. All other properties under history are assumed to be
+  // versions. To provide a custom package-list.json file, remove this property and include a
+  // package-list.json file in ig-data.
+  //
+  // The current version is special. If the author provides only a single string value, it is
+  // assumed to be the URL path to the current build. The following default values will then be
+  // used:
+  // * desc: Continuous Integration Build (latest in version control)
+  // * status: ci-build
+  // * current: true
+  history?: ConfigurationHistory;
+
   // The indexPageContent property is used to generate a basic index.md file. To provide a
   // custom index file, do not include this property and include an index.md or index.xml file
   // in input/pages/ or input/pagecontent folders.
@@ -138,3 +154,30 @@ export type ConfigurationMenuItem = {
   openInNewTab?: boolean;
   subMenu?: ConfigurationMenuItem[];
 };
+
+export type ConfigurationHistory = {
+  'package-id'?: string;
+  canonical?: string;
+  title?: string;
+  introduction?: string;
+  list?: ConfigurationHistoryItem[];
+};
+
+export type ConfigurationHistoryItem = {
+  version: string;
+  date?: string;
+  desc?: string;
+  path: string;
+  changes?: string;
+  status?:
+    | 'ci-build'
+    | 'preview'
+    | 'ballot'
+    | 'trial-use'
+    | 'update'
+    | 'normative'
+    | 'trial-use+normative';
+  sequence?: string;
+  fhirversion?: string;
+  current?: boolean;
+};
diff --git a/src/ig/IGExporter.ts b/src/ig/IGExporter.ts
index 76a3f3c..0780a7e 100644
--- a/src/ig/IGExporter.ts
+++ b/src/ig/IGExporter.ts
@@ -1264,14 +1264,32 @@ export class IGExporter {
   }
 
   /**
-   * Adds the package-list.json file to the IG. Copied from the
-   *  package-list.json found at ig-data/package-list.json.
+   * Adds the package-list.json file to the IG. Generated based on the Configuration history
+   * field, or the package-list.json found at ig-data/package-list.json.
    *
    * @param igPath {string} - the path where the IG is exported to
    */
   addPackageList(igPath: string): void {
     const packageListPath = path.join(this.igDataPath, 'package-list.json');
-    if (existsSync(packageListPath)) {
+    const isIgDataPackageList = existsSync(packageListPath);
+
+    if (this.config.history) {
+      const outputPath = path.join(igPath, 'package-list.json');
+      outputJSONSync(outputPath, this.config.history, { spaces: 2 });
+      logger.info('Generated package-list.json');
+      this.updateOutputLog(outputPath, [this.configPath], 'generated');
+      if (isIgDataPackageList) {
+        logger.warn(
+          `Found both a "history" property in config.yaml and a package-list.json file at ig-data${path.sep}package-list.json. ` +
+            'Since the "history" property is present in the config.yaml, a package-list.json file will be generated and ' +
+            `the ig-data${path.sep}package-list.json file will be ignored. Remove the "history" property in config.yaml ` +
+            `to use the ig-data${path.sep}package-list.json file instead.`,
+          {
+            file: packageListPath
+          }
+        );
+      }
+    } else if (isIgDataPackageList) {
       this.copyAsIs(packageListPath, path.join(igPath, 'package-list.json'));
       logger.info('Copied ig-data/package-list.json.');
     }
diff --git a/src/import/YAMLConfiguration.ts b/src/import/YAMLConfiguration.ts
index 0327257..2a6b216 100644
--- a/src/import/YAMLConfiguration.ts
+++ b/src/import/YAMLConfiguration.ts
@@ -20,13 +20,14 @@ import {
 
 /**
  * YAMLConfiguration follows the proposed configuration format for FSH and incorporates aspects
- * of the ImplementationGuide, ig.ini, package.json, and menu.xml formats.
+ * of the ImplementationGuide, ig.ini, package.json, package-list.json, and menu.xml formats.
  *
  * This format is intended to be represented using YAML but is transformed into JSON for our use.
  *
  * @see {@link http://hl7.org/fhir/R4/implementationguide.html}
  * @see {@link https://build.fhir.org/ig/FHIR/ig-guidance/using-templates.html#igroot}
  * @see {@link https://confluence.hl7.org/display/FHIR/NPM+Package+Specification}
+ * @see {@link https://confluence.hl7.org/pages/viewpage.action?pageId=66928420#FHIRIGPackageListdoco-PublicationObject}
  * @see {@link https://github.com/FHIR/sample-ig/blob/master/input/includes/menu.xml}
  */
 export type YAMLConfiguration = {
@@ -153,6 +154,21 @@ export type YAMLConfiguration = {
   // a menu)?
   menu?: YAMLConfigurationMenuTree;
 
+  // The history property corresponds to package-list.json. SUSHI will use the existing top-level
+  // properties in its config to populate the top-level package-list.json properties: package-id,
+  // canonical, title, and introduction. Authors that wish to provide different values can supply
+  // them as properties under history. All other properties under history are assumed to be
+  // versions. To provide a custom package-list.json file, remove this property and include a
+  // package-list.json file in ig-data.
+  //
+  // The current version is special. If the author provides only a single string value, it is
+  // assumed to be the URL path to the current build. The following default values will then be
+  // used:
+  // * desc: Continuous Integration Build (latest in version control)
+  // * status: ci-build
+  // * current: true
+  history?: YAMLConfigurationHistory;
+
   // The indexPageContent property is used to generate a basic index.md file. To provide a
   // custom index file, do not include this property and include an index.md or index.xml file
   // in input/pages/ or input/pagecontent folders.
@@ -346,3 +362,38 @@ export type YAMLConfigurationParameterMap = {
 export type YAMLConfigurationMenuTree = {
   [key: string]: string | YAMLConfigurationMenuTree;
 };
+
+export type YAMLConfigurationHistory = {
+  'package-id'?: string;
+  canonical?: string;
+  title?: string;
+  introduction?: string;
+  current: string | YAMLConfigurationHistoryItem;
+  [key: string]: string | YAMLConfigurationHistoryItem;
+};
+
+export type YAMLConfigurationHistoryItem = {
+  date?: string | number; // YAML will parse year-only dates as numbers
+  desc?: string;
+  path: string;
+  changes?: string;
+  status?:
+    | 'ci-build'
+    | '#ci-build'
+    | 'preview'
+    | '#preview'
+    | 'ballot'
+    | '#ballot'
+    | 'trial-use'
+    | '#trial-use'
+    | 'update'
+    | '#update'
+    | 'normative'
+    | '#normative'
+    | 'trial=use+normative'
+    | '#trial=use+normative';
+
+  sequence?: string;
+  fhirversion?: string;
+  current?: boolean;
+};
diff --git a/src/import/ensureConfigurationFile.ts b/src/import/ensureConfigurationFile.ts
index 33749f1..66045dd 100644
--- a/src/import/ensureConfigurationFile.ts
+++ b/src/import/ensureConfigurationFile.ts
@@ -1,6 +1,6 @@
 import path from 'path';
 import ini from 'ini';
-import fs, { existsSync, readFileSync } from 'fs-extra';
+import fs, { existsSync, readFileSync, readJSONSync } from 'fs-extra';
 import { Document, createNode } from 'yaml';
 import { YAMLMap, Pair, Schema } from 'yaml/types';
 import wordwrap from 'wordwrap';
@@ -10,7 +10,9 @@ import { PackageJSON } from '../fshtypes';
 import {
   YAMLConfigurationPublisher,
   YAMLConfigurationContactDetail,
-  YAMLConfigurationMenuTree
+  YAMLConfigurationHistoryItem,
+  YAMLConfigurationMenuTree,
+  YAMLConfigurationHistory
 } from './YAMLConfiguration';
 import { logger } from '../utils/FSHLogger';
 import { pad, padEnd } from 'lodash';
@@ -69,15 +71,16 @@ export function ensureConfigurationFile(root: string, allowFromScratch = false):
 }
 
 /**
- * Creates a new config.yaml file using available other configs (package.json, ig.ini, menu.xml)
+ * Creates a new config.yaml file using available other configs (package.json, ig.ini, package-list.json, menu.xml)
  * @param root - the root path of the FSH Tank
  * @param allowFromScratch - create a config file from scratch even if package.json isn't found
  * @returns {string|undefined} path to the config file or undefined if it couldn't find or create one
  */
 function generateConfiguration(root: string, allowFromScratch: boolean): string {
-  // Generate the config from available ig.ini and package.json
+  // Generate the config from available ig.ini, package.json, and package-list.json
   const igIni = getIgIni(root);
   const packageJSON = getPackageJSON(root);
+  const packageList = getPackageList(root);
 
   // The config is "from scratch" if there is no package.json to base it on.
   // We know there wasn't a packageJSON if the returned packageJSON is the default one (by reference)
@@ -319,6 +322,54 @@ function generateConfiguration(root: string, allowFromScratch: boolean): string
     // if an index file is provided, don't add anything to config
   }
 
+  // history
+  // @ts-ignore it's ok that we don't initialize it with current.  It will get current eventually.
+  const yamlHistory: YAMLConfigurationHistory = {};
+  if (packageList['package-id'] != null && packageList['package-id'] !== packageJSON.name) {
+    yamlHistory['package-id'] = packageList['package-id'];
+  }
+  if (packageList.canonical != null && packageList.canonical !== packageJSON.canonical) {
+    yamlHistory.canonical = packageList.canonical;
+  }
+  if (packageList.title != null && packageList.title !== packageJSON.title) {
+    yamlHistory.title = packageList.title;
+  }
+  if (packageList.introduction != null && packageList.introduction !== packageJSON.description) {
+    yamlHistory.introduction = packageList.introduction;
+  }
+  yamlHistory.current = getCurrentFromPackageList(packageList, packageJSON);
+  (packageList.list as any[])?.forEach(item => {
+    if (item.version === 'current') {
+      return;
+    }
+    const yamlItem = cloneDeep(item);
+    delete yamlItem.version;
+    yamlHistory[item.version] = yamlItem;
+  });
+  if (packageList !== DEFAULT_PACKAGE_LIST) {
+    commentedPairs.push(
+      new CommentPair('history', yamlHistory)
+        .withCommentBefore(
+          getBoxComment(
+            'package-list.json',
+            'To control the package-list.json using this config, uncomment and set the "history" property.'
+          )
+        )
+        .withSpaceBefore()
+    );
+  } else {
+    setPairs.push(
+      new YAMLPair('history', yamlHistory)
+        .withCommentBefore(
+          getBoxComment(
+            'package-list.json',
+            'To use a provided ig-data/package-list.json file, delete the "history" property below.'
+          )
+        )
+        .withSpaceBefore()
+    );
+  }
+
   // FSHOnly - in SUSHI 0.12.x, presence or absence of ig-data indicated if you wanted an IG or not.
   // But don't ever set FSHOnly when creating config.yaml "from scratch".
   if (!fs.existsSync(path.join(root, 'ig-data')) && !isFromScratch) {
@@ -447,6 +498,38 @@ function getMenuObjectFromMenuXML(menuXML: string): YAMLConfigurationMenuTree {
   }
 }
 
+/**
+ * Gets the value for current, preferring a simple path string when all other values match the default
+ * @param packageList - the packageList object from package-list.json (or default)
+ * @param packageJSON - the packageJSON object from package.json (or default)
+ * @returns {string|YAMLConfigurationHistoryItem} - a string for the current path or an object w/ current details
+ */
+function getCurrentFromPackageList(
+  packageList: any,
+  packageJSON: PackageJSON
+): string | YAMLConfigurationHistoryItem {
+  const plCurrent = packageList.list.find((item: any) => item.version === 'current');
+  if (plCurrent == null) {
+    return packageJSON.url ?? DEFAULT_PACKAGE_LIST.list[0].path;
+  }
+  for (const key of Object.keys(plCurrent)) {
+    if (
+      key === 'version' ||
+      key === 'path' ||
+      (key === 'desc' && plCurrent.desc === DEFAULT_PACKAGE_LIST.list[0].desc) ||
+      (key === 'status' && plCurrent.status === DEFAULT_PACKAGE_LIST.list[0].status) ||
+      (key === 'current' && plCurrent.current === DEFAULT_PACKAGE_LIST.list[0].current)
+    ) {
+      continue;
+    } else {
+      // return the full object since it contains non-default values besides path
+      return plCurrent;
+    }
+  }
+  // If we got this far, the only significant value is the path, so just return that
+  return plCurrent.path;
+}
+
 /**
  * Gets the ig.ini representation or a default if none is found
  * @param root - the root path for the FSH Tank
@@ -484,6 +567,21 @@ function getPackageJSON(root: string): PackageJSON {
   return DEFAULT_PACKAGE_JSON;
 }
 
+/**
+ * Gets the package-list.json representation or a default if none is found
+ * @param root - the root path for the FSH Tank
+ * @returns {object} - the package-list.json object representation
+ */
+function getPackageList(root: string): any {
+  const packageListPath = path.join(root, 'ig-data', 'package-list.json');
+  if (existsSync(packageListPath)) {
+    return readJSONSync(packageListPath);
+  }
+
+  // No existing package-list.json, so return one with default values to populate an example config
+  return DEFAULT_PACKAGE_LIST;
+}
+
 const DEFAULT_IG_INI: any = {
   template: 'fhir.base.template#current',
   copyrightyear: `${new Date().getFullYear()}+`,
@@ -520,3 +618,25 @@ const DEFAULT_MENU: YAMLConfigurationMenuTree = {
     'FHIR Spec': 'new-tab external http://hl7.org/fhir/R4/index.html'
   }
 };
+
+const DEFAULT_PACKAGE_LIST: any = {
+  list: [
+    {
+      version: 'current',
+      desc: 'Continuous Integration Build (latest in version control)',
+      path: 'http://build.fhir.org/ig/example/example-ig',
+      status: 'ci-build',
+      current: true
+    },
+    {
+      version: DEFAULT_PACKAGE_JSON.version,
+      fhirversion: '4.0.1',
+      date: '2099-01-01',
+      desc: 'Initial STU ballot (Mmm yyyy Ballot)',
+      path: 'http://example.org/fhir/STU1',
+      status: 'ballot',
+      sequence: 'STU 1',
+      current: true
+    }
+  ]
+};
diff --git a/src/import/importConfiguration.ts b/src/import/importConfiguration.ts
index 5816ab4..ec920a8 100644
--- a/src/import/importConfiguration.ts
+++ b/src/import/importConfiguration.ts
@@ -20,7 +20,9 @@ import {
   Configuration,
   ConfigurationGroup,
   ConfigurationResource,
-  ConfigurationMenuItem
+  ConfigurationMenuItem,
+  ConfigurationHistory,
+  ConfigurationHistoryItem
 } from '../fshtypes/Configuration';
 import { logger } from '../utils/FSHLogger';
 import { parseCodeLexeme } from './parseCodeLexeme';
@@ -158,6 +160,7 @@ export function importConfiguration(yaml: YAMLConfiguration | string, file: stri
     templates: parseTemplates(yaml.templates, file),
     template: yaml.template,
     menu: parseMenu(yaml.menu),
+    history: parseHistory(yaml, file),
     indexPageContent: yaml.indexPageContent,
     FSHOnly: yaml.FSHOnly ?? false
   };
@@ -180,14 +183,6 @@ export function importConfiguration(yaml: YAMLConfiguration | string, file: stri
     }
   }
 
-  // @ts-ignore
-  if (yaml.history) {
-    logger.warn(
-      'The "history" property is set in the configuration. Use of this property to generate a package-list.json file is deprecated, and the property will be ignored',
-      { file }
-    );
-  }
-
   return config;
 }
 
@@ -741,6 +736,83 @@ function parseMenu(yamlMenu: YAMLConfigurationMenuTree): ConfigurationMenuItem[]
   });
 }
 
+function parseHistory(yamlConfig: YAMLConfiguration, file: string): ConfigurationHistory {
+  const yamlHistory = yamlConfig.history;
+  if (yamlHistory == null) {
+    return;
+  }
+  const history: ConfigurationHistory = {
+    'package-id': yamlHistory['package-id'] ?? yamlConfig.packageId ?? yamlConfig.id,
+    canonical: yamlHistory.canonical ?? yamlConfig.canonical,
+    title: yamlHistory.title ?? yamlConfig.title,
+    introduction: yamlHistory.introduction ?? yamlConfig.description,
+    list: []
+  };
+  if (yamlHistory.current) {
+    if (typeof yamlHistory.current === 'string') {
+      history.list.push({
+        version: 'current',
+        desc: 'Continuous Integration Build (latest in version control)',
+        path: yamlHistory.current,
+        status: 'ci-build',
+        current: true
+      });
+    } else {
+      history.list.push({
+        version: 'current',
+        date: normalizeToString(yamlHistory.current.date),
+        desc:
+          yamlHistory.current.desc ?? 'Continuous Integration Build (latest in version control)',
+        path: required(yamlHistory.current.path, 'history[current].path', file),
+        changes: yamlHistory.current.changes,
+        status: parseCodeWithRequiredValues(
+          yamlHistory.current.status ?? 'ci-build',
+          allowedHistoryStatus,
+          'history[current].status',
+          file
+        ),
+        sequence: yamlHistory.current.sequence,
+        fhirversion: yamlHistory.current.fhirversion,
+        current: yamlHistory.current.current ?? true
+      });
+    }
+  }
+  for (const [key, value] of Object.entries(yamlHistory)) {
+    if (['package-id', 'canonical', 'title', 'introduction', 'current'].indexOf(key) !== -1) {
+      continue;
+    }
+    const item = value as ConfigurationHistoryItem;
+    history.list.push({
+      version: key,
+      date: required(item.date, `history[${key}].date`, file),
+      desc: required(item.desc, `history[${key}].desc`, file),
+      path: required(item.path, `history[${key}].path`, file),
+      changes: item.changes,
+      status: parseCodeWithRequiredValues(
+        required(item.status, `history[${key}].status`, file),
+        allowedHistoryStatus,
+        `history[${key}].status`,
+        file
+      ),
+      sequence: required(item.sequence, `history[${key}].sequence`, file),
+      fhirversion: required(item.fhirversion, `history[${key}].fhirVersion`, file),
+      current: item.current
+    });
+  }
+  history.list.forEach(item => removeUndefinedValues(item));
+  return history;
+}
+
+const allowedHistoryStatus: ConfigurationHistoryItem['status'][] = [
+  'ci-build',
+  'preview',
+  'ballot',
+  'trial-use',
+  'update',
+  'normative',
+  'trial-use+normative'
+];
+
 function removeUndefinedValues<T extends object>(incoming: T): T {
   Object.keys(incoming).forEach((k: string) => {
     // @ts-ignore Element implicitly has an 'any' type


Test patch:
diff --git a/test/ig/IGExporter.IG.test.ts b/test/ig/IGExporter.IG.test.ts
index fe35b6b..d030f3e 100644
--- a/test/ig/IGExporter.IG.test.ts
+++ b/test/ig/IGExporter.IG.test.ts
@@ -132,7 +132,8 @@ describe('IGExporter', () => {
             code: 'releaselabel',
             value: 'CI Build'
           }
-        ]
+        ],
+        history: {} // to suppress warning for HL7 IGs
       };
       pkg = new Package(config);
       pkg.profiles.push(...pkgProfiles);
diff --git a/test/ig/IGExporter.SUSHI-GENERATED-FILES.test.ts b/test/ig/IGExporter.SUSHI-GENERATED-FILES.test.ts
index 42c8972..caa2da1 100644
--- a/test/ig/IGExporter.SUSHI-GENERATED-FILES.test.ts
+++ b/test/ig/IGExporter.SUSHI-GENERATED-FILES.test.ts
@@ -84,6 +84,30 @@ describe('IGExporter', () => {
       const config = cloneDeep(minimalConfig);
       config.indexPageContent = 'My Index Page';
       config.menu = [{ name: 'Animals', url: 'animals.html' }];
+      config.history = {
+        'package-id': 'fhir.us.minimal',
+        canonical: 'http://hl7.org/fhir/us/minimal',
+        title: 'Minimal IG',
+        introduction: 'Minimal IG exercises history.',
+        list: [
+          {
+            version: 'current',
+            desc: 'Continuous Integration Build (latest in version control)',
+            path: 'http://build.fhir.org/ig/HL7/minimal-ig/',
+            status: 'ci-build',
+            current: true
+          },
+          {
+            version: '0.9.1',
+            fhirversion: '4.0.0',
+            date: '2019-06-10',
+            desc: 'Initial STU ballot (Sep 2019 Ballot)',
+            path: 'https://hl7.org/fhir/us/minimal/2019Sep/',
+            status: 'ballot',
+            sequence: 'STU 1'
+          }
+        ]
+      };
       const pkg = new Package(config);
       const fixtures = path.join(__dirname, 'fixtures', 'simple-ig');
       const exporter = new IGExporter(pkg, defs, path.resolve(fixtures, 'ig-data'), false);
@@ -113,6 +137,9 @@ describe('IGExporter', () => {
       expect(content).toMatch(
         /\| input[\/\\]pagecontent[\/\\]index\.md \s*\| generated \s*\| .*[\/\\]simple-ig[\/\\]config\.yaml \s*\|/
       );
+      expect(content).toMatch(
+        /\| package-list\.json \s*\| generated \s*\| .*[\/\\]simple-ig[\/\\]config\.yaml \s*\|/
+      );
     });
 
     it('should generate a SUSHI-GENERATED-FILES.md with the correct listings when configs are not generated or copied', () => {
@@ -167,6 +194,7 @@ describe('IGExporter', () => {
       expect(content).not.toMatch('ig.ini');
       expect(content).not.toMatch('menu.xml');
       expect(content).not.toMatch('index.md');
+      expect(content).not.toMatch('package-list.json');
     });
   });
 });
diff --git a/test/ig/IGExporter.package-list.test.ts b/test/ig/IGExporter.package-list.test.ts
index 0bb83a2..302e148 100644
--- a/test/ig/IGExporter.package-list.test.ts
+++ b/test/ig/IGExporter.package-list.test.ts
@@ -5,6 +5,8 @@ import { IGExporter } from '../../src/ig';
 import { Package } from '../../src/export';
 import { loggerSpy } from '../testhelpers/loggerSpy';
 import { minimalConfig } from '../utils/minimalConfig';
+import { Configuration } from '../../src/fshtypes';
+import { cloneDeep } from 'lodash';
 
 describe('IGExporter', () => {
   // Track temp files/folders for cleanup
@@ -12,17 +14,114 @@ describe('IGExporter', () => {
 
   describe('#package-list', () => {
     let tempOut: string;
+    let config: Configuration;
 
     beforeAll(() => {
       tempOut = temp.mkdirSync('sushi-test');
     });
 
+    beforeEach(() => {
+      config = cloneDeep(minimalConfig);
+    });
+
     afterAll(() => {
       temp.cleanupSync();
     });
 
+    it('should not export package-list.json when config.history is undefined and ig-data/package-list.json does not exist', () => {
+      config.canonical = 'https://foo.com/my-ig';
+      const pkg = new Package(config);
+      const exporter = new IGExporter(pkg, null, '');
+      exporter.addPackageList(tempOut);
+      const pkgListPath = path.join(tempOut, 'package-list.json');
+      expect(fs.existsSync(pkgListPath)).toBeFalsy();
+      expect(loggerSpy.getAllMessages('warn')).toHaveLength(0);
+    });
+
+    it('should export package-list.json when config.history is defined', () => {
+      config.history = {
+        'package-id': 'fhir.us.example',
+        canonical: 'http://hl7.org/fhir/us/example',
+        title: 'HL7 FHIR Implementation Guide: Example IG Release 1 - US Realm | STU1',
+        introduction: 'Example IG exercises many of the fields in a SUSHI configuration.',
+        list: [
+          {
+            version: 'current',
+            desc: 'Continuous Integration Build (latest in version control)',
+            path: 'http://build.fhir.org/ig/HL7/example-ig/',
+            status: 'ci-build',
+            current: true
+          },
+          {
+            version: '0.9.1',
+            fhirversion: '4.0.0',
+            date: '2019-06-10',
+            desc: 'Initial STU ballot (Sep 2019 Ballot)',
+            path: 'https://hl7.org/fhir/us/example/2019Sep/',
+            status: 'ballot',
+            sequence: 'STU 1'
+          }
+        ]
+      };
+      const pkg = new Package(config);
+      const exporter = new IGExporter(pkg, null, '');
+      exporter.addPackageList(tempOut);
+      const pkgListPath = path.join(tempOut, 'package-list.json');
+      expect(fs.existsSync(pkgListPath)).toBeTruthy();
+      const content = fs.readJSONSync(pkgListPath);
+      expect(content).toEqual(config.history);
+      const outputLogDetails = exporter.getOutputLogDetails(pkgListPath);
+      expect(outputLogDetails.action).toBe('generated');
+      expect(outputLogDetails.inputs).toHaveLength(1);
+      expect(outputLogDetails.inputs[0].endsWith('config.yaml')).toBeTruthy();
+      expect(loggerSpy.getLastMessage('info')).toBe('Generated package-list.json');
+    });
+
+    it('should export package-list.json and emit a warning when both config.history and ig-data/package-list.json are defined', () => {
+      config.history = {
+        'package-id': 'fhir.us.example',
+        canonical: 'http://hl7.org/fhir/us/example',
+        title: 'HL7 FHIR Implementation Guide: Example IG Release 1 - US Realm | STU1',
+        introduction: 'Example IG exercises many of the fields in a SUSHI configuration.',
+        list: [
+          {
+            version: 'current',
+            desc: 'Continuous Integration Build (latest in version control)',
+            path: 'http://build.fhir.org/ig/HL7/example-ig/',
+            status: 'ci-build',
+            current: true
+          },
+          {
+            version: '0.9.1',
+            fhirversion: '4.0.0',
+            date: '2019-06-10',
+            desc: 'Initial STU ballot (Sep 2019 Ballot)',
+            path: 'https://hl7.org/fhir/us/example/2019Sep/',
+            status: 'ballot',
+            sequence: 'STU 1'
+          }
+        ]
+      };
+      const pkg = new Package(config);
+      const igDataPath = path.resolve(__dirname, 'fixtures', 'customized-ig', 'ig-data');
+      const exporter = new IGExporter(pkg, null, igDataPath);
+      exporter.addPackageList(tempOut);
+      const pkgListPath = path.join(tempOut, 'package-list.json');
+      expect(fs.existsSync(pkgListPath)).toBeTruthy();
+      const content = fs.readJSONSync(pkgListPath);
+      expect(content).toEqual(config.history);
+      const outputLogDetails = exporter.getOutputLogDetails(pkgListPath);
+      expect(outputLogDetails.action).toBe('generated');
+      expect(outputLogDetails.inputs).toHaveLength(1);
+      expect(outputLogDetails.inputs[0].endsWith('config.yaml')).toBeTruthy();
+      expect(loggerSpy.getLastMessage('info')).toBe('Generated package-list.json');
+      expect(loggerSpy.getLastMessage('warn')).toMatch(
+        /Found both a "history" property in config\.yaml and a package-list\.json file.*File: .*package-list.json/s
+      );
+    });
+
     it('should copy package-list.json when ig-data/package-list.json is defined', () => {
-      const pkg = new Package(minimalConfig);
+      const pkg = new Package(config);
       const igDataPath = path.resolve(__dirname, 'fixtures', 'customized-ig', 'ig-data');
       const exporter = new IGExporter(pkg, null, igDataPath);
       exporter.addPackageList(tempOut);
diff --git a/test/ig/IGExporter.test.ts b/test/ig/IGExporter.test.ts
index 1348421..dd9957f 100644
--- a/test/ig/IGExporter.test.ts
+++ b/test/ig/IGExporter.test.ts
@@ -28,6 +28,9 @@ describe('IGExporter', () => {
 
     it('should export default files when no configuration values affect those files', () => {
       // defaults are:
+      // no package-list.json
+      const packageListPath = path.join(tempOut, 'package-list.json');
+      expect(fs.existsSync(packageListPath)).toBeFalsy();
       // no menu.xml
       const menuPath = path.join(tempOut, 'input', 'includes', 'menu.xml');
       expect(fs.existsSync(menuPath)).toBeFalsy();
@@ -92,6 +95,14 @@ describe('IGExporter', () => {
       });
     });
 
+    it('should create package-list.json based upon configuration', () => {
+      const packageListPath = path.join(tempOut, 'package-list.json');
+      expect(fs.existsSync(packageListPath)).toBeTruthy();
+      const packageListContent = fs.readJSONSync(packageListPath);
+      expect(packageListContent['package-id']).toBe('fhir.us.example');
+      expect(packageListContent.list).toHaveLength(3);
+    });
+
     it('should create menu.xml based upon configuration', () => {
       const menuPath = path.join(tempOut, 'input', 'includes', 'menu.xml');
       expect(fs.existsSync(menuPath)).toBeTruthy();
diff --git a/test/import/YAMLConfiguration.test.ts b/test/import/YAMLConfiguration.test.ts
index cde7d44..435cc1a 100644
--- a/test/import/YAMLConfiguration.test.ts
+++ b/test/import/YAMLConfiguration.test.ts
@@ -104,6 +104,26 @@ describe('YAMLConfiguration', () => {
         excludettl: true,
         validation: ['allow-any-extensions', 'no-broken-links']
       });
+      expect(config.history).toEqual({
+        current: 'http://build.fhir.org/ig/HL7/example-ig/',
+        '1.0.0': {
+          fhirversion: '4.0.1',
+          date: '2020-03-06',
+          desc: 'STU 1 Release',
+          path: 'https://hl7.org/fhir/us/example/STU1/',
+          status: 'trial-use',
+          sequence: 'STU 1',
+          current: true
+        },
+        '0.9.1': {
+          fhirversion: '4.0.0',
+          date: '2019-06-10',
+          desc: 'Initial STU ballot (Sep 2019 Ballot)',
+          path: 'https://hl7.org/fhir/us/example/2019Sep/',
+          status: 'ballot',
+          sequence: 'STU 1'
+        }
+      });
     });
   });
 });
diff --git a/test/import/ensureConfigurationFile.test.ts b/test/import/ensureConfigurationFile.test.ts
index d359a37..8ae9a60 100644
--- a/test/import/ensureConfigurationFile.test.ts
+++ b/test/import/ensureConfigurationFile.test.ts
@@ -81,6 +81,18 @@ describe('ensureConfigurationFile', () => {
           'FHIR Spec': 'new-tab external http://hl7.org/fhir/R4/index.html'
         }
       },
+      history: {
+        current: 'http://build.fhir.org/ig/example/example-ig',
+        '0.0.1': {
+          fhirversion: '4.0.1',
+          date: '2099-01-01',
+          desc: 'Initial STU ballot (Mmm yyyy Ballot)',
+          path: 'http://example.org/fhir/STU1',
+          status: 'ballot',
+          sequence: 'STU 1',
+          current: true
+        }
+      },
       indexPageContent:
         'This is an example IG description. You should uncomment and replace it with your own.'
     });
@@ -94,6 +106,9 @@ describe('ensureConfigurationFile', () => {
     expect(configText).toMatch(
       'To use a provided ig-data/input/includes/menu.xml file, delete the "menu" property below.'
     );
+    expect(configText).toMatch(
+      'To use a provided ig-data/package-list.json file, delete the "history" property below.'
+    );
   });
 
   it('should generate an appropriate config for a tank w/ only a package.json', () => {
@@ -135,6 +150,18 @@ describe('ensureConfigurationFile', () => {
         'Artifact Index': 'artifacts.html',
         Support: { 'FHIR Spec': 'new-tab external http://hl7.org/fhir/R4/index.html' }
       },
+      history: {
+        current: 'http://build.fhir.org/ig/example/example-ig',
+        '0.0.1': {
+          fhirversion: '4.0.1',
+          date: '2099-01-01',
+          desc: 'Initial STU ballot (Mmm yyyy Ballot)',
+          path: 'http://example.org/fhir/STU1',
+          status: 'ballot',
+          sequence: 'STU 1',
+          current: true
+        }
+      },
       indexPageContent: 'Provides a simple example of how FSH can be used to create an IG',
       FSHOnly: true
     });
@@ -149,6 +176,9 @@ describe('ensureConfigurationFile', () => {
     expect(configText).toMatch(
       'To use a provided ig-data/input/includes/menu.xml file, delete the "menu" property below.'
     );
+    expect(configText).toMatch(
+      'To use a provided ig-data/package-list.json file, delete the "history" property below.'
+    );
   });
 
   it('should generate an appropriate config for a tank w/ minimal package.json', () => {
@@ -196,6 +226,18 @@ describe('ensureConfigurationFile', () => {
         'Artifact Index': 'artifacts.html',
         Support: { 'FHIR Spec': 'new-tab external http://hl7.org/fhir/R4/index.html' }
       },
+      history: {
+        current: 'http://build.fhir.org/ig/example/example-ig',
+        '0.0.1': {
+          fhirversion: '4.0.1',
+          date: '2099-01-01',
+          desc: 'Initial STU ballot (Mmm yyyy Ballot)',
+          path: 'http://example.org/fhir/STU1',
+          status: 'ballot',
+          sequence: 'STU 1',
+          current: true
+        }
+      },
       indexPageContent: '',
       FSHOnly: true
     });
@@ -283,6 +325,18 @@ describe('ensureConfigurationFile', () => {
         'Artifact Index': 'artifacts.html',
         Support: { 'FHIR Spec': 'new-tab external http://hl7.org/fhir/R4/index.html' }
       },
+      history: {
+        current: 'http://build.fhir.org/ig/example/example-ig',
+        '0.0.1': {
+          fhirversion: '4.0.1',
+          date: '2099-01-01',
+          desc: 'Initial STU ballot (Mmm yyyy Ballot)',
+          path: 'http://example.org/fhir/STU1',
+          status: 'ballot',
+          sequence: 'STU 1',
+          current: true
+        }
+      },
       indexPageContent: 'Provides a simple example of how FSH can be used to create an IG',
       FSHOnly: true
     });
@@ -330,6 +384,18 @@ describe('ensureConfigurationFile', () => {
         'Artifact Index': 'artifacts.html',
         Support: { 'FHIR Spec': 'new-tab external http://hl7.org/fhir/R4/index.html' }
       },
+      history: {
+        current: 'http://build.fhir.org/ig/example/example-ig',
+        '0.0.1': {
+          fhirversion: '4.0.1',
+          date: '2099-01-01',
+          desc: 'Initial STU ballot (Mmm yyyy Ballot)',
+          path: 'http://example.org/fhir/STU1',
+          status: 'ballot',
+          sequence: 'STU 1',
+          current: true
+        }
+      },
       indexPageContent: 'Provides a simple example of how FSH can be used to create an IG'
     });
 
@@ -341,6 +407,9 @@ describe('ensureConfigurationFile', () => {
     expect(configText).toMatch(
       'To use a provided ig-data/input/includes/menu.xml file, delete the "menu" property below.'
     );
+    expect(configText).toMatch(
+      'To use a provided ig-data/package-list.json file, delete the "history" property below.'
+    );
   });
 
   it('should generate an appropriate config for a tank w/ a package.json and minimal ig.ini', () => {
@@ -388,6 +457,18 @@ describe('ensureConfigurationFile', () => {
         'Artifact Index': 'artifacts.html',
         Support: { 'FHIR Spec': 'new-tab external http://hl7.org/fhir/R4/index.html' }
       },
+      history: {
+        current: 'http://build.fhir.org/ig/example/example-ig',
+        '0.0.1': {
+          fhirversion: '4.0.1',
+          date: '2099-01-01',
+          desc: 'Initial STU ballot (Mmm yyyy Ballot)',
+          path: 'http://example.org/fhir/STU1',
+          status: 'ballot',
+          sequence: 'STU 1',
+          current: true
+        }
+      },
       indexPageContent: 'Provides a simple example of how FSH can be used to create an IG'
     });
 
@@ -395,6 +476,183 @@ describe('ensureConfigurationFile', () => {
     expect(configText).toMatch('# template:');
   });
 
+  it('should generate an appropriate config for a tank w/ package.json and package-list.json', () => {
+    // Copy the fixture to a temp folder since we actually create files in the tank
+    const tank = temp.mkdirSync('sushi-test');
+    fs.copySync(path.join(__dirname, 'fixtures', 'package-json-and-package-list'), tank);
+    const configPath = ensureConfigurationFile(tank);
+    expect(configPath).toBe(path.join(tank, 'config.yaml'));
+    expect(loggerSpy.getLastMessage('warn')).toBe(
+      `Generated new configuration file: ${configPath}. Please review to ensure configuration is correct.`
+    );
+    const configText = fs.readFileSync(configPath, 'utf8');
+    const configJSON = YAML.parse(configText);
+
+    // Test the formal YAML contents
+    expect(configJSON).toEqual({
+      id: 'sushi-test',
+      canonical: 'http://hl7.org/fhir/sushi-test',
+      version: '0.1.0',
+      name: 'FSHTestIG',
+      title: 'FSH Test IG',
+      status: 'active',
+      publisher: {
+        name: 'James Tuna',
+        url: 'https://tunafish.org/',
+        email: 'tuna@reef.gov'
+      },
+      description: 'Provides a simple example of how FSH can be used to create an IG',
+      license: 'CC0-1.0',
+      fhirVersion: '4.0.1',
+      dependencies: { 'hl7.fhir.us.core': '3.1.0', 'hl7.fhir.uv.vhdir': 'current' },
+      parameters: { 'show-inherited-invariants': false },
+      copyrightYear: '2020+',
+      releaseLabel: 'CI Build',
+      template: 'fhir.base.template#current',
+      menu: {
+        'IG Home': 'index.html',
+        'Table of Contents': 'toc.html',
+        'Artifact Index': 'artifacts.html',
+        Support: { 'FHIR Spec': 'new-tab external http://hl7.org/fhir/R4/index.html' }
+      },
+      // history should no longer exist since it will just use the provided package-list.json
+      indexPageContent: 'Provides a simple example of how FSH can be used to create an IG'
+    });
+
+    // history should be commented out since we use the one from ig.ini
+    expect(configText).toMatch('# history:');
+    expect(configText).toMatch(
+      'To control the package-list.json using this config, uncomment and set the "history" property.'
+    );
+
+    // The commented out history should correspond to the existing package-list.json
+    const configTextLines = configText.split('\n');
+    const historyText = configTextLines
+      .slice(configTextLines.findIndex(l => l.startsWith('# history:')))
+      .map(l => l.replace(/^# /, ''))
+      .join('\n');
+    const historyJSON = YAML.parse(historyText);
+    expect(historyJSON.history).toEqual({
+      title: 'FSH Test IG History',
+      introduction: 'Custom intro',
+      current: {
+        version: 'current',
+        desc: 'CI Build (latest in version control)',
+        path: 'http://build.fhir.org/ig/fhir/sushi-test',
+        status: 'ci-build',
+        current: true
+      },
+      '0.2.0': {
+        fhirversion: '4.0.1',
+        date: '2019-09-01',
+        desc: 'Second STU ballot (Sep 2019 Ballot)',
+        path: 'http://hl7.org/fhir/sushi-test/stu2',
+        status: 'ballot',
+        sequence: 'STU 2'
+      },
+      '0.1.0': {
+        fhirversion: '4.0.1',
+        date: '2019-05-01',
+        desc: 'Initial STU ballot (May 2019 Ballot)',
+        path: 'http://hl7.org/fhir/sushi-test/stu1',
+        status: 'ballot',
+        sequence: 'STU 1'
+      }
+    });
+
+    expect(configText).toMatch(
+      'To use a provided ig-data/ig.ini file, delete the "template" property below.'
+    );
+    expect(configText).toMatch(
+      'To use a provided ig-data/input/includes/menu.xml file, delete the "menu" property below.'
+    );
+  });
+
+  it('should generate an appropriate config for a tank w/ package.json and simplest package-list.json', () => {
+    // Copy the fixture to a temp folder since we actually create files in the tank
+    const tank = temp.mkdirSync('sushi-test');
+    fs.copySync(path.join(__dirname, 'fixtures', 'package-json-and-package-list'), tank);
+
+    // Tweak the package list to align w/ expected package-list defaults
+    const packageJSON = fs.readJsonSync(path.join(tank, 'package.json'));
+    const packageList = fs.readJsonSync(path.join(tank, 'ig-data', 'package-list.json'));
+    packageList.title = packageJSON.title;
+    packageList.introduction = packageList.description;
+    packageList.list[0].desc = 'Continuous Integration Build (latest in version control)';
+    fs.writeJsonSync(path.join(tank, 'ig-data', 'package-list.json'), packageList);
+
+    // ensureConfigurationFile
+    const configPath = ensureConfigurationFile(tank);
+    expect(configPath).toBe(path.join(tank, 'config.yaml'));
+    expect(loggerSpy.getLastMessage('warn')).toBe(
+      `Generated new configuration file: ${configPath}. Please review to ensure configuration is correct.`
+    );
+    const configText = fs.readFileSync(configPath, 'utf8');
+    const configJSON = YAML.parse(configText);
+
+    // Test the formal YAML contents
+    expect(configJSON).toEqual({
+      id: 'sushi-test',
+      canonical: 'http://hl7.org/fhir/sushi-test',
+      version: '0.1.0',
+      name: 'FSHTestIG',
+      title: 'FSH Test IG',
+      status: 'active',
+      publisher: {
+        name: 'James Tuna',
+        url: 'https://tunafish.org/',
+        email: 'tuna@reef.gov'
+      },
+      description: 'Provides a simple example of how FSH can be used to create an IG',
+      license: 'CC0-1.0',
+      fhirVersion: '4.0.1',
+      dependencies: { 'hl7.fhir.us.core': '3.1.0', 'hl7.fhir.uv.vhdir': 'current' },
+      parameters: { 'show-inherited-invariants': false },
+      copyrightYear: '2020+',
+      releaseLabel: 'CI Build',
+      template: 'fhir.base.template#current',
+      menu: {
+        'IG Home': 'index.html',
+        'Table of Contents': 'toc.html',
+        'Artifact Index': 'artifacts.html',
+        Support: { 'FHIR Spec': 'new-tab external http://hl7.org/fhir/R4/index.html' }
+      },
+      // history should still not exist since it will just use the provided package-list.json
+      indexPageContent: 'Provides a simple example of how FSH can be used to create an IG'
+    });
+
+    // history should be commented out since we use the one from ig.ini
+    expect(configText).toMatch('# history:');
+
+    // The commented out history should correspond to the existing package-list.json
+    const configTextLines = configText.split('\n');
+    const historyText = configTextLines
+      .slice(configTextLines.findIndex(l => l.startsWith('# history:')))
+      .map(l => l.replace(/^# /, ''))
+      .join('\n');
+    const historyJSON = YAML.parse(historyText);
+    expect(historyJSON.history).toEqual({
+      // no title or introduction since they match package title and description
+      current: 'http://build.fhir.org/ig/fhir/sushi-test', // path only since all other properties match defaults
+      '0.2.0': {
+        fhirversion: '4.0.1',
+        date: '2019-09-01',
+        desc: 'Second STU ballot (Sep 2019 Ballot)',
+        path: 'http://hl7.org/fhir/sushi-test/stu2',
+        status: 'ballot',
+        sequence: 'STU 2'
+      },
+      '0.1.0': {
+        fhirversion: '4.0.1',
+        date: '2019-05-01',
+        desc: 'Initial STU ballot (May 2019 Ballot)',
+        path: 'http://hl7.org/fhir/sushi-test/stu1',
+        status: 'ballot',
+        sequence: 'STU 1'
+      }
+    });
+  });
+
   it('should generate an appropriate config for a tank w/ package.json and menu.xml', () => {
     // Copy the fixture to a temp folder since we actually create files in the tank
     const tank = temp.mkdirSync('sushi-test');
@@ -429,6 +687,18 @@ describe('ensureConfigurationFile', () => {
       releaseLabel: 'CI Build',
       template: 'fhir.base.template#current',
       // menu should no longer exist since the provided menu.xml is used
+      history: {
+        current: 'http://build.fhir.org/ig/example/example-ig',
+        '0.0.1': {
+          fhirversion: '4.0.1',
+          date: '2099-01-01',
+          desc: 'Initial STU ballot (Mmm yyyy Ballot)',
+          path: 'http://example.org/fhir/STU1',
+          status: 'ballot',
+          sequence: 'STU 1',
+          current: true
+        }
+      },
       indexPageContent: 'Provides a simple example of how FSH can be used to create an IG'
     });
 
@@ -441,8 +711,9 @@ describe('ensureConfigurationFile', () => {
     // The commented out menu should correspond to the generated menu based on provided menu.xml
     const configTextLines = configText.split('\n');
     const start = configTextLines.findIndex(l => l.startsWith('# menu:'));
+    const end = configTextLines.findIndex(l => l.startsWith('# NOTE: The history property'));
     const menuText = configTextLines
-      .slice(start)
+      .slice(start, end)
       .map(l => l.replace(/^# /, ''))
       .join('\n');
     const menuJSON = YAML.parse(menuText);
@@ -465,6 +736,9 @@ describe('ensureConfigurationFile', () => {
     expect(configText).toMatch(
       'To use a provided ig-data/ig.ini file, delete the "template" property below.'
     );
+    expect(configText).toMatch(
+      'To use a provided ig-data/package-list.json file, delete the "history" property below.'
+    );
   });
 
   it('should generate an appropriate config for a tank w/ package.json and index.md', () => {
@@ -505,6 +779,18 @@ describe('ensureConfigurationFile', () => {
         'Table of Contents': 'toc.html',
         'Artifact Index': 'artifacts.html',
         Support: { 'FHIR Spec': 'new-tab external http://hl7.org/fhir/R4/index.html' }
+      },
+      history: {
+        current: 'http://build.fhir.org/ig/example/example-ig',
+        '0.0.1': {
+          fhirversion: '4.0.1',
+          date: '2099-01-01',
+          desc: 'Initial STU ballot (Mmm yyyy Ballot)',
+          path: 'http://example.org/fhir/STU1',
+          status: 'ballot',
+          sequence: 'STU 1',
+          current: true
+        }
       }
       // indexPageContent should not exist since the provided index.md is used
     });
diff --git a/test/import/fixtures/example-config.yaml b/test/import/fixtures/example-config.yaml
index 136b3b8..6223f5b 100644
--- a/test/import/fixtures/example-config.yaml
+++ b/test/import/fixtures/example-config.yaml
@@ -1,5 +1,5 @@
 # This IG YML file is inspired by the ImplementationGuide resource
-# and also draws from package.json and ig.ini.
+# and also draws from package.json, package-list.json, and ig.ini.
 # That said, it is structured for ease-of-use, so it is not strictly
 # conformant to any of those existing configuration formats.
 
@@ -182,6 +182,39 @@ parameters:
   excludettl: true
   validation: [allow-any-extensions, no-broken-links]
 
+# The history property corresponds to package-list.json. SUSHI will
+# use the existing top-level properties in its config to populate the
+# top-level package-list.json properties: packageId, canonical, title,
+# and introduction. Authors that wish to provide different values can
+# supply them as properties under history. All other properties under
+# history are assumed to be versions. To provide a custom package-list.json
+# file, remove this property and include a package-list.json file in ig-data.
+history:
+  # The current version is special. If the author provides only a
+  # single value, it is assumed to be the URL path to the current
+  # build. The following default values will then be used:
+  #   desc: Continuous Integration Build (latest in version control)
+  #   status: ci-build
+  #   current: true
+  current: http://build.fhir.org/ig/HL7/example-ig/
+  # All other versions need each of their values fully specified.
+  # See: https://confluence.hl7.org/pages/viewpage.action?pageId=66928420#FHIRIGPackageListdoco-PublicationObject
+  1.0.0:
+    fhirversion: 4.0.1
+    date: 2020-03-06
+    desc: STU 1 Release
+    path: https://hl7.org/fhir/us/example/STU1/
+    status: trial-use
+    sequence: STU 1
+    current: true
+  0.9.1:
+    fhirversion: 4.0.0
+    date: 2019-06-10
+    desc: Initial STU ballot (Sep 2019 Ballot)
+    path: https://hl7.org/fhir/us/example/2019Sep/
+    status: ballot
+    sequence: STU 1
+
 # The indexPageContent property is used to generate a basic index.md file. To provide a
 # custom index file, do not include this property and include an index.md or index.xml file
 # in input/pages/ or input/pagecontent folders.
diff --git a/test/import/fixtures/package-json-and-package-list/ig-data/package-list.json b/test/import/fixtures/package-json-and-package-list/ig-data/package-list.json
new file mode 100644
index 0000000..37838fe
--- /dev/null
+++ b/test/import/fixtures/package-json-and-package-list/ig-data/package-list.json
@@ -0,0 +1,33 @@
+{
+  "package-id": "sushi-test",
+  "title": "FSH Test IG History",
+  "canonical": "http://hl7.org/fhir/sushi-test",
+  "introduction": "Custom intro",
+  "list": [
+    {
+      "version": "current",
+      "desc": "CI Build (latest in version control)",
+      "path": "http://build.fhir.org/ig/fhir/sushi-test",
+      "status": "ci-build",
+      "current": true
+    },
+    {
+      "version": "0.2.0",
+      "fhirversion": "4.0.1",
+      "date": "2019-09-01",
+      "desc": "Second STU ballot (Sep 2019 Ballot)",
+      "path": "http://hl7.org/fhir/sushi-test/stu2",
+      "status": "ballot",
+      "sequence": "STU 2"
+    },
+    {
+      "version": "0.1.0",
+      "fhirversion": "4.0.1",
+      "date": "2019-05-01",
+      "desc": "Initial STU ballot (May 2019 Ballot)",
+      "path": "http://hl7.org/fhir/sushi-test/stu1",
+      "status": "ballot",
+      "sequence": "STU 1"
+    }
+  ]
+}
diff --git a/test/import/fixtures/package-json-and-package-list/package.json b/test/import/fixtures/package-json-and-package-list/package.json
new file mode 100644
index 0000000..84d5d53
--- /dev/null
+++ b/test/import/fixtures/package-json-and-package-list/package.json
@@ -0,0 +1,22 @@
+{
+  "name": "sushi-test",
+  "version" : "0.1.0",
+  "canonical" : "http://hl7.org/fhir/sushi-test",
+  "url" : "http://hl7.org/fhir/sushi-test",
+  "title" : "FSH Test IG",
+  "description": "Provides a simple example of how FSH can be used to create an IG",
+  "dependencies": {
+     "hl7.fhir.r4.core": "4.0.1",
+     "hl7.fhir.us.core": "3.1.0",
+     "hl7.fhir.uv.vhdir":"current"
+  },
+  "author": "James Tuna",
+  "maintainers": [
+    {
+      "name": "James Tuna",
+      "email": "tuna@reef.gov",
+      "url": "https://tunafish.org/"
+    }
+  ],
+  "license": "CC0-1.0"
+ }
\ No newline at end of file
diff --git a/test/import/importConfiguration.test.ts b/test/import/importConfiguration.test.ts
index 8efb0a5..30c019a 100644
--- a/test/import/importConfiguration.test.ts
+++ b/test/import/importConfiguration.test.ts
@@ -161,6 +161,40 @@ describe('importConfiguration', () => {
         { code: 'validation', value: 'allow-any-extensions' },
         { code: 'validation', value: 'no-broken-links' }
       ],
+      history: {
+        'package-id': 'fhir.us.example',
+        canonical: 'http://hl7.org/fhir/us/example',
+        title: 'HL7 FHIR Implementation Guide: Example IG Release 1 - US Realm | STU1',
+        introduction: 'Example IG exercises many of the fields in a SUSHI configuration.',
+        list: [
+          {
+            version: 'current',
+            desc: 'Continuous Integration Build (latest in version control)',
+            path: 'http://build.fhir.org/ig/HL7/example-ig/',
+            status: 'ci-build',
+            current: true
+          },
+          {
+            version: '1.0.0',
+            fhirversion: '4.0.1',
+            date: '2020-03-06',
+            desc: 'STU 1 Release',
+            path: 'https://hl7.org/fhir/us/example/STU1/',
+            status: 'trial-use',
+            sequence: 'STU 1',
+            current: true
+          },
+          {
+            version: '0.9.1',
+            fhirversion: '4.0.0',
+            date: '2019-06-10',
+            desc: 'Initial STU ballot (Sep 2019 Ballot)',
+            path: 'https://hl7.org/fhir/us/example/2019Sep/',
+            status: 'ballot',
+            sequence: 'STU 1'
+          }
+        ]
+      },
       indexPageContent: 'Example Index Page Content',
       FSHOnly: false
     };
@@ -1849,16 +1883,355 @@ describe('importConfiguration', () => {
 
   describe('#history', () => {
     it('should use default values for history where applicable', () => {
-      // @ts-ignore
+      minYAML.title = 'HL7 FHIR Implementation Guide: Minimal IG Release 1 - US Realm | STU1';
+      minYAML.description = 'Minimal IG exercises only required fields in a SUSHI configuration.';
       minYAML.history = {
         current: 'http://build.fhir.org/ig/HL7/minimal-ig/'
       };
       const config = importConfiguration(minYAML, 'test-config.yaml');
-      // @ts-ignore
-      expect(config.history).toBeUndefined();
-      expect(loggerSpy.getLastMessage('warn')).toMatch(
-        /The "history" property is set.*test-config\.yaml/s
+      expect(config.history).toEqual({
+        'package-id': 'fhir.us.minimal',
+        canonical: 'http://hl7.org/fhir/us/minimal',
+        title: 'HL7 FHIR Implementation Guide: Minimal IG Release 1 - US Realm | STU1',
+        introduction: 'Minimal IG exercises only required fields in a SUSHI configuration.',
+        list: [
+          {
+            version: 'current',
+            desc: 'Continuous Integration Build (latest in version control)',
+            path: 'http://build.fhir.org/ig/HL7/minimal-ig/',
+            status: 'ci-build',
+            current: true
+          }
+        ]
+      });
+    });
+    it('should allow mix of default and provided values for history.current', () => {
+      minYAML.title = 'HL7 FHIR Implementation Guide: Minimal IG Release 1 - US Realm | STU1';
+      minYAML.description = 'Minimal IG exercises only required fields in a SUSHI configuration.';
+      minYAML.history = {
+        current: {
+          fhirversion: '4.0.1',
+          date: '2020-04-01',
+          path: 'http://build.fhir.org/ig/HL7/example-ig/',
+          sequence: 'STU 2'
+        }
+      };
+      const config = importConfiguration(minYAML, 'test-config.yaml');
+      expect(config.history).toEqual({
+        'package-id': 'fhir.us.minimal',
+        canonical: 'http://hl7.org/fhir/us/minimal',
+        title: 'HL7 FHIR Implementation Guide: Minimal IG Release 1 - US Realm | STU1',
+        introduction: 'Minimal IG exercises only required fields in a SUSHI configuration.',
+        list: [
+          {
+            version: 'current',
+            fhirversion: '4.0.1',
+            date: '2020-04-01',
+            desc: 'Continuous Integration Build (latest in version control)',
+            path: 'http://build.fhir.org/ig/HL7/example-ig/',
+            status: 'ci-build',
+            sequence: 'STU 2',
+            current: true
+          }
+        ]
+      });
+    });
+    it('should use provided values for history where applicable', () => {
+      minYAML.title = 'HL7 FHIR Implementation Guide: Minimal IG Release 1 - US Realm | STU1';
+      minYAML.description = 'Minimal IG exercises only required fields in a SUSHI configuration.';
+      minYAML.history = {
+        'package-id': 'fhir.us.other',
+        canonical: 'http://hl7.org/fhir/us/other',
+        title: 'HL7 FHIR Implementation Guide: Other IG Release 1 - US Realm | STU1',
+        introduction: 'Other IG is other than the other IG.',
+        current: {
+          fhirversion: '4.0.1',
+          date: '2020-04-01',
+          desc: 'CI Build Release',
+          path: 'http://build.fhir.org/ig/HL7/example-ig/',
+          status: 'ci-build',
+          sequence: 'STU 2',
+          current: true
+        },
+        '1.0.0': {
+          fhirversion: '4.0.1',
+          date: '2020-03-06',
+          desc: 'STU 1 Release',
+          path: 'https://hl7.org/fhir/us/example/STU1/',
+          status: 'trial-use',
+          sequence: 'STU 1',
+          current: true
+        },
+        '0.9.1': {
+          fhirversion: '4.0.0',
+          date: '2019-06-10',
+          desc: 'Initial STU ballot (Sep 2019 Ballot)',
+          path: 'https://hl7.org/fhir/us/example/2019Sep/',
+          status: 'ballot',
+          sequence: 'STU 1'
+        }
+      };
+      const config = importConfiguration(minYAML, 'test-config.yaml');
+      expect(config.history).toEqual({
+        'package-id': 'fhir.us.other',
+        canonical: 'http://hl7.org/fhir/us/other',
+        title: 'HL7 FHIR Implementation Guide: Other IG Release 1 - US Realm | STU1',
+        introduction: 'Other IG is other than the other IG.',
+        list: [
+          {
+            version: 'current',
+            fhirversion: '4.0.1',
+            date: '2020-04-01',
+            desc: 'CI Build Release',
+            path: 'http://build.fhir.org/ig/HL7/example-ig/',
+            status: 'ci-build',
+            sequence: 'STU 2',
+            current: true
+          },
+          {
+            version: '1.0.0',
+            fhirversion: '4.0.1',
+            date: '2020-03-06',
+            desc: 'STU 1 Release',
+            path: 'https://hl7.org/fhir/us/example/STU1/',
+            status: 'trial-use',
+            sequence: 'STU 1',
+            current: true
+          },
+          {
+            version: '0.9.1',
+            fhirversion: '4.0.0',
+            date: '2019-06-10',
+            desc: 'Initial STU ballot (Sep 2019 Ballot)',
+            path: 'https://hl7.org/fhir/us/example/2019Sep/',
+            status: 'ballot',
+            sequence: 'STU 1'
+          }
+        ]
+      });
+    });
+    it('should report invalid history.current.status code', () => {
+      minYAML.history = {
+        current: {
+          path: 'http://build.fhir.org/ig/HL7/example-ig/',
+          // @ts-ignore Type '"OK"' is not assignable to type '"ci-build" | "preview" | ... '.
+          status: 'OK'
+        }
+      };
+      const config = importConfiguration(minYAML, 'test-config.yaml');
+      expect(loggerSpy.getLastMessage('error')).toMatch(
+        /Invalid history\[current\]\.status value: 'OK'\. Must be one of: 'ci-build','preview','ballot','trial-use','update','normative','trial-use\+normative'\.\s*File: test-config\.yaml/
       );
+      expect(config.history.list[0]).toEqual({
+        version: 'current',
+        desc: 'Continuous Integration Build (latest in version control)',
+        path: 'http://build.fhir.org/ig/HL7/example-ig/',
+        current: true
+      });
+    });
+    it('should report an error if history.current is an object and path is missing', () => {
+      minYAML.history = {
+        // @ts-ignore Type '...' is not assignable to type 'YAMLConfigurationHistoryItem'.
+        current: {
+          fhirversion: '4.0.1'
+        }
+      };
+      const config = importConfiguration(minYAML, 'test-config.yaml');
+      expect(loggerSpy.getLastMessage('error')).toMatch(
+        /Configuration missing required property: history\[current]\.path\s*File: test-config\.yaml/
+      );
+      expect(config.history.list[0]).toEqual({
+        version: 'current',
+        desc: 'Continuous Integration Build (latest in version control)',
+        status: 'ci-build',
+        fhirversion: '4.0.1',
+        current: true
+      });
+    });
+    it('should report invalid history.[version].status code', () => {
+      minYAML.history = {
+        current: 'http://build.fhir.org/ig/HL7/minimal-ig/',
+        '1.0.0': {
+          fhirversion: '4.0.1',
+          date: '2020-03-06',
+          desc: 'STU 1 Release',
+          path: 'https://hl7.org/fhir/us/example/STU1/',
+          // @ts-ignore Type '"ready"' is not assignable to type '"ci-build" | "preview" | ... '.
+          status: 'ready',
+          sequence: 'STU 1',
+          current: true
+        }
+      };
+      const config = importConfiguration(minYAML, 'test-config.yaml');
+      expect(loggerSpy.getLastMessage('error')).toMatch(
+        /Invalid history\[1\.0\.0\]\.status value: 'ready'\. Must be one of: 'ci-build','preview','ballot','trial-use','update','normative','trial-use\+normative'\.\s*File: test-config\.yaml/
+      );
+      expect(config.history.list[1]).toEqual({
+        version: '1.0.0',
+        date: '2020-03-06',
+        desc: 'STU 1 Release',
+        path: 'https://hl7.org/fhir/us/example/STU1/',
+        sequence: 'STU 1',
+        fhirversion: '4.0.1',
+        current: true
+      });
+    });
+    it('should report an error if history.[version].date is missing', () => {
+      minYAML.history = {
+        current: 'http://build.fhir.org/ig/HL7/minimal-ig/',
+        '1.0.0': {
+          fhirversion: '4.0.1',
+          desc: 'STU 1 Release',
+          path: 'https://hl7.org/fhir/us/example/STU1/',
+          status: 'trial-use',
+          sequence: 'STU 1',
+          current: true
+        }
+      };
+      const config = importConfiguration(minYAML, 'test-config.yaml');
+      expect(loggerSpy.getLastMessage('error')).toMatch(
+        /Configuration missing required property: history\[1\.0\.0\]\.date\s*File: test-config\.yaml/
+      );
+      expect(config.history.list[1]).toEqual({
+        version: '1.0.0',
+        desc: 'STU 1 Release',
+        path: 'https://hl7.org/fhir/us/example/STU1/',
+        status: 'trial-use',
+        sequence: 'STU 1',
+        fhirversion: '4.0.1',
+        current: true
+      });
+    });
+    it('should report an error if history.[version].desc is missing', () => {
+      minYAML.history = {
+        current: 'http://build.fhir.org/ig/HL7/minimal-ig/',
+        '1.0.0': {
+          fhirversion: '4.0.1',
+          date: '2020-03-06',
+          path: 'https://hl7.org/fhir/us/example/STU1/',
+          status: 'trial-use',
+          sequence: 'STU 1',
+          current: true
+        }
+      };
+      const config = importConfiguration(minYAML, 'test-config.yaml');
+      expect(loggerSpy.getLastMessage('error')).toMatch(
+        /Configuration missing required property: history\[1\.0\.0\]\.desc\s*File: test-config\.yaml/
+      );
+      expect(config.history.list[1]).toEqual({
+        version: '1.0.0',
+        date: '2020-03-06',
+        path: 'https://hl7.org/fhir/us/example/STU1/',
+        status: 'trial-use',
+        sequence: 'STU 1',
+        fhirversion: '4.0.1',
+        current: true
+      });
+    });
+    it('should report an error if history.[version].path is missing', () => {
+      minYAML.history = {
+        current: 'http://build.fhir.org/ig/HL7/minimal-ig/',
+        // @ts-ignore Type '...' is not assignable to type ...
+        '1.0.0': {
+          fhirversion: '4.0.1',
+          date: '2020-03-06',
+          desc: 'STU 1 Release',
+          status: 'trial-use',
+          sequence: 'STU 1',
+          current: true
+        }
+      };
+      const config = importConfiguration(minYAML, 'test-config.yaml');
+      expect(loggerSpy.getLastMessage('error')).toMatch(
+        /Configuration missing required property: history\[1\.0\.0\]\.path\s*File: test-config\.yaml/
+      );
+      expect(config.history.list[1]).toEqual({
+        version: '1.0.0',
+        date: '2020-03-06',
+        desc: 'STU 1 Release',
+        status: 'trial-use',
+        sequence: 'STU 1',
+        fhirversion: '4.0.1',
+        current: true
+      });
+    });
+    it('should report an error if history.[version].status is missing', () => {
+      minYAML.history = {
+        current: 'http://build.fhir.org/ig/HL7/minimal-ig/',
+        '1.0.0': {
+          fhirversion: '4.0.1',
+          date: '2020-03-06',
+          desc: 'STU 1 Release',
+          path: 'https://hl7.org/fhir/us/example/STU1/',
+          sequence: 'STU 1',
+          current: true
+        }
+      };
+      const config = importConfiguration(minYAML, 'test-config.yaml');
+      expect(loggerSpy.getLastMessage('error')).toMatch(
+        /Configuration missing required property: history\[1\.0\.0\]\.status\s*File: test-config\.yaml/
+      );
+      expect(config.history.list[1]).toEqual({
+        version: '1.0.0',
+        date: '2020-03-06',
+        desc: 'STU 1 Release',
+        path: 'https://hl7.org/fhir/us/example/STU1/',
+        sequence: 'STU 1',
+        fhirversion: '4.0.1',
+        current: true
+      });
+    });
+    it('should report an error if history.[version].sequence is missing', () => {
+      minYAML.history = {
+        current: 'http://build.fhir.org/ig/HL7/minimal-ig/',
+        '1.0.0': {
+          fhirversion: '4.0.1',
+          date: '2020-03-06',
+          desc: 'STU 1 Release',
+          path: 'https://hl7.org/fhir/us/example/STU1/',
+          status: 'trial-use',
+          current: true
+        }
+      };
+      const config = importConfiguration(minYAML, 'test-config.yaml');
+      expect(loggerSpy.getLastMessage('error')).toMatch(
+        /Configuration missing required property: history\[1\.0\.0\]\.sequence\s*File: test-config\.yaml/
+      );
+      expect(config.history.list[1]).toEqual({
+        version: '1.0.0',
+        date: '2020-03-06',
+        desc: 'STU 1 Release',
+        path: 'https://hl7.org/fhir/us/example/STU1/',
+        status: 'trial-use',
+        fhirversion: '4.0.1',
+        current: true
+      });
+    });
+    it('should report an error if history.[version].fhirVersion is missing', () => {
+      minYAML.history = {
+        current: 'http://build.fhir.org/ig/HL7/minimal-ig/',
+        '1.0.0': {
+          date: '2020-03-06',
+          desc: 'STU 1 Release',
+          path: 'https://hl7.org/fhir/us/example/STU1/',
+          status: 'trial-use',
+          sequence: 'STU 1',
+          current: true
+        }
+      };
+      const config = importConfiguration(minYAML, 'test-config.yaml');
+      expect(loggerSpy.getLastMessage('error')).toMatch(
+        /Configuration missing required property: history\[1\.0\.0\]\.fhirVersion\s*File: test-config\.yaml/
+      );
+      expect(config.history.list[1]).toEqual({
+        version: '1.0.0',
+        date: '2020-03-06',
+        desc: 'STU 1 Release',
+        path: 'https://hl7.org/fhir/us/example/STU1/',
+        status: 'trial-use',
+        sequence: 'STU 1',
+        current: true
+      });
     });
   });