Repository: FHIR/sushi
Pull request #1241 (2023-03-22 18:20:04)

Support dependencies on specific CI build branches
Add support for dependency versions that specify a current branch name. For example: `current$mybranchname`.

See: https://chat.fhir.org/#narrow/stream/179166-implementers/topic/Package.20cache.20-.20multiple.20dev.20versions/near/291131585

If you want to manually test this, add this dependency to a project:
```yaml
dependencies:
  hl7.fhir.be.mycarenet: current$issue-30
```

If you run SUSHI on the master branch, you'll get an error when it tries to download the dependency (assuming you don't have it cached locally).

If you run this PR branch, it will work. If you run SUSHI with `-d`, you'll see a debug message that the cached date matches the build date, so it will not download again.


Patch:
diff --git a/src/fhirdefs/load.ts b/src/fhirdefs/load.ts
index 5f98cd2..2687b2d 100644
--- a/src/fhirdefs/load.ts
+++ b/src/fhirdefs/load.ts
@@ -60,18 +60,28 @@ export async function loadDependency(
         `Downloading ${fullPackageName} since SUSHI cannot determine if the version in the local cache is the most recent build.`
       );
     }
-  } else if (version === 'current') {
+  } else if (/^current(\$.+)?$/.test(version)) {
+    // Authors can reference a specific CI branch by specifying version as current${branchname} (e.g., current$mybranch)
+    // See: https://chat.fhir.org/#narrow/stream/179166-implementers/topic/Package.20cache.20-.20multiple.20dev.20versions/near/291131585
+    let branch: string;
+    if (version.indexOf('$') !== -1) {
+      branch = version.slice(version.indexOf('$') + 1);
+    }
     // Even if a local current package is loaded, we must still check that the local package date matches
     // the date on the most recent version on build.fhir.org. If the date does not match, we re-download to the cache
+    type QAEntry = { 'package-id': string; date: string; repo: string };
     const baseUrl = 'https://build.fhir.org/ig';
     const res = await axiosGet(`${baseUrl}/qas.json`);
-    const qaData: { 'package-id': string; date: string; repo: string }[] = res?.data;
+    const qaData: QAEntry[] = res?.data;
     // Find matching packages and sort by date to get the most recent
-    let newestPackage;
+    let newestPackage: QAEntry;
     if (qaData?.length > 0) {
-      const matchingPackages = qaData
-        .filter(p => p['package-id'] === packageName)
-        .filter(p => p.repo.match(/(master|main)\/qa\.json$/));
+      let matchingPackages = qaData.filter(p => p['package-id'] === packageName);
+      if (branch == null) {
+        matchingPackages = matchingPackages.filter(p => p.repo.match(/\/(master|main)\/qa\.json$/));
+      } else {
+        matchingPackages = matchingPackages.filter(p => p.repo.endsWith(`/${branch}/qa.json`));
+      }
       newestPackage = matchingPackages.sort((p1, p2) => {
         return Date.parse(p2['date']) - Date.parse(p1['date']);
       })[0];


Test patch:
diff --git a/test/fhirdefs/fixtures/sushi-test#current$oldbranch/package/StructureDefinition-MyPatient.json b/test/fhirdefs/fixtures/sushi-test#current$oldbranch/package/StructureDefinition-MyPatient.json
new file mode 100644
index 0000000..4f65cc4
--- /dev/null
+++ b/test/fhirdefs/fixtures/sushi-test#current$oldbranch/package/StructureDefinition-MyPatient.json
@@ -0,0 +1,25 @@
+{
+  "resourceType": "StructureDefinition",
+  "id": "MyPatient",
+  "url": "http://hl7.org/fhir/sushi-test/StructureDefinition/MyPatient",
+  "name": "MyPatient",
+  "status": "active",
+  "fhirVersion": "4.0.1",
+  "kind": "resource",
+  "type": "Patient",
+  "baseDefinition": "http://hl7.org/fhir/StructureDefinition/Patient",
+  "derivation": "constraint",
+  "differential": {
+    "element": [
+      {
+        "id": "Patient.deceased[x]",
+        "path": "Patient.deceased[x]",
+        "type": [
+          {
+            "code": "boolean"
+          }
+        ]
+      }
+    ]
+  }
+}
diff --git a/test/fhirdefs/fixtures/sushi-test#current$oldbranch/package/package.json b/test/fhirdefs/fixtures/sushi-test#current$oldbranch/package/package.json
new file mode 100644
index 0000000..28cf91c
--- /dev/null
+++ b/test/fhirdefs/fixtures/sushi-test#current$oldbranch/package/package.json
@@ -0,0 +1,24 @@
+{
+  "name": "sushi-test",
+  "version" : "0.2.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"
+  },
+  "date": "20200412230227",
+  "language": "en",
+  "author": "James Tuna",
+  "maintainers": [
+    {
+      "name": "Bill Cod",
+      "email": "cod@reef.gov",
+      "url": "https://capecodfishermen.org/"
+    }
+  ],
+  "license": "CC0-1.0"
+ }
\ No newline at end of file
diff --git a/test/fhirdefs/fixtures/sushi-test#current$testbranch/package/StructureDefinition-MyPatient.json b/test/fhirdefs/fixtures/sushi-test#current$testbranch/package/StructureDefinition-MyPatient.json
new file mode 100644
index 0000000..4f65cc4
--- /dev/null
+++ b/test/fhirdefs/fixtures/sushi-test#current$testbranch/package/StructureDefinition-MyPatient.json
@@ -0,0 +1,25 @@
+{
+  "resourceType": "StructureDefinition",
+  "id": "MyPatient",
+  "url": "http://hl7.org/fhir/sushi-test/StructureDefinition/MyPatient",
+  "name": "MyPatient",
+  "status": "active",
+  "fhirVersion": "4.0.1",
+  "kind": "resource",
+  "type": "Patient",
+  "baseDefinition": "http://hl7.org/fhir/StructureDefinition/Patient",
+  "derivation": "constraint",
+  "differential": {
+    "element": [
+      {
+        "id": "Patient.deceased[x]",
+        "path": "Patient.deceased[x]",
+        "type": [
+          {
+            "code": "boolean"
+          }
+        ]
+      }
+    ]
+  }
+}
diff --git a/test/fhirdefs/fixtures/sushi-test#current$testbranch/package/package.json b/test/fhirdefs/fixtures/sushi-test#current$testbranch/package/package.json
new file mode 100644
index 0000000..c6d46f0
--- /dev/null
+++ b/test/fhirdefs/fixtures/sushi-test#current$testbranch/package/package.json
@@ -0,0 +1,24 @@
+{
+  "name": "sushi-test",
+  "version" : "0.2.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"
+  },
+  "date": "20200413230227",
+  "language": "en",
+  "author": "James Tuna",
+  "maintainers": [
+    {
+      "name": "Bill Cod",
+      "email": "cod@reef.gov",
+      "url": "https://capecodfishermen.org/"
+    }
+  ],
+  "license": "CC0-1.0"
+ }
\ No newline at end of file
diff --git a/test/fhirdefs/load.test.ts b/test/fhirdefs/load.test.ts
index d62726c..f41e151 100644
--- a/test/fhirdefs/load.test.ts
+++ b/test/fhirdefs/load.test.ts
@@ -80,7 +80,7 @@ describe('#loadDependency()', () => {
   // the actual implementation than I'd prefer, but... at least it's in one place.
   const expectDownloadSequence = (
     sources: string | string[],
-    destination: string,
+    destination: string | null,
     isCurrent = false,
     isCurrentFound = true
   ): void => {
@@ -164,6 +164,20 @@ describe('#loadDependency()', () => {
               'ig-ver': '0.1.0',
               repo: 'sushi/sushi-test/branches/master/qa.json'
             },
+            {
+              url: 'http://hl7.org/fhir/sushi-test/ImplementationGuide/sushi-test-0.1.0',
+              name: 'sushi-test',
+              'package-id': 'sushi-test',
+              'ig-ver': '0.2.0',
+              repo: 'sushi/sushi-test/branches/testbranch/qa.json'
+            },
+            {
+              url: 'http://hl7.org/fhir/sushi-test/ImplementationGuide/sushi-test-0.1.0',
+              name: 'sushi-test',
+              'package-id': 'sushi-test',
+              'ig-ver': '0.2.0',
+              repo: 'sushi/sushi-test/branches/oldbranch/qa.json'
+            },
             {
               url: 'http://hl7.org/fhir/sushi-no-main/ImplementationGuide/sushi-no-main-0.1.0',
               name: 'sushi-no-main',
@@ -184,6 +198,8 @@ describe('#loadDependency()', () => {
         };
       } else if (
         uri === 'https://packages.fhir.org/sushi-test/0.2.0' ||
+        uri === 'https://build.fhir.org/ig/sushi/sushi-test/branches/testbranch/package.tgz' ||
+        uri === 'https://build.fhir.org/ig/sushi/sushi-test/branches/oldbranch/package.tgz' ||
         uri === 'https://build.fhir.org/ig/sushi/sushi-test-old/branches/master/package.tgz' ||
         uri === 'https://build.fhir.org/ig/HL7/US-Core-R4/branches/main/package.tgz' ||
         uri === 'https://build.fhir.org/hl7.fhir.r5.core.tgz' ||
@@ -341,6 +357,18 @@ describe('#loadDependency()', () => {
     ]);
   });
 
+  it('should not try to download a current$branch package that is already in the cache and up to date', async () => {
+    const expectedDefs = new FHIRDefinitions();
+    loadFromPath(cachePath, 'sushi-test#current$testbranch', expectedDefs);
+    await expect(
+      loadDependency('sushi-test', 'current$testbranch', defs, cachePath)
+    ).resolves.toEqual(expectedDefs);
+    expect(axiosSpy.mock.calls).toEqual([
+      ['https://build.fhir.org/ig/qas.json'],
+      ['https://build.fhir.org/ig/sushi/sushi-test/branches/testbranch/package.manifest.json']
+    ]);
+  });
+
   it('should try to load the latest package from build.fhir.org when a current package version is not locally cached', async () => {
     await expect(loadDependency('hl7.fhir.us.core.r4', 'current', defs, 'foo')).rejects.toThrow(
       'The package hl7.fhir.us.core.r4#current could not be loaded locally or from the FHIR package registry'
@@ -352,6 +380,17 @@ describe('#loadDependency()', () => {
     );
   });
 
+  it('should try to load the latest branch-specific package from build.fhir.org when a current package version is not locally cached', async () => {
+    await expect(loadDependency('sushi-test', 'current$testbranch', defs, 'foo')).rejects.toThrow(
+      'The package sushi-test#current$testbranch could not be loaded locally or from the FHIR package registry'
+    ); // the package is never actually added to the cache, since tar is mocked
+    expectDownloadSequence(
+      'https://build.fhir.org/ig/sushi/sushi-test/branches/testbranch/package.tgz',
+      path.join('foo', 'sushi-test#current$testbranch'),
+      true
+    );
+  });
+
   it('should try to load the latest package from build.fhir.org when a current package version has an older version that is locally cached', async () => {
     await expect(
       loadDependency('sushi-test-old', 'current', defs, cachePath)
@@ -364,16 +403,32 @@ describe('#loadDependency()', () => {
     expect(removeSpy.mock.calls[0][0]).toBe(path.join(cachePath, 'sushi-test-old#current'));
   });
 
+  it('should try to load the latest branch-specific package from build.fhir.org when a current package version has an older version that is locally cached', async () => {
+    await expect(
+      loadDependency('sushi-test', 'current$oldbranch', defs, cachePath)
+    ).resolves.toBeTruthy(); // Since tar is mocked, the actual cache is not updated
+    expectDownloadSequence(
+      'https://build.fhir.org/ig/sushi/sushi-test/branches/oldbranch/package.tgz',
+      path.join(cachePath, 'sushi-test#current$oldbranch'),
+      true
+    );
+    expect(removeSpy.mock.calls[0][0]).toBe(path.join(cachePath, 'sushi-test#current$oldbranch'));
+  });
+
   it('should not try to load the latest package from build.fhir.org from a branch that is not main/master', async () => {
     await expect(loadDependency('sushi-no-main', 'current', defs, cachePath)).rejects.toThrow(
       'The package sushi-no-main#current is not available on https://build.fhir.org/ig/qas.json, so no current version can be loaded'
     );
-    expectDownloadSequence(
-      'https://build.fhir.org/ig/sushi/sushi-no-main/branches/master/package.tgz',
-      null,
-      true,
-      false
+    expectDownloadSequence('', null, true, false);
+  });
+
+  it('should not try to load the branch-specific package from build.fhir.org when that branch is not available in qas', async () => {
+    await expect(
+      loadDependency('sushi-test', 'current$wrongbranch', defs, cachePath)
+    ).rejects.toThrow(
+      'The package sushi-test#current$wrongbranch is not available on https://build.fhir.org/ig/qas.json, so no current version can be loaded'
     );
+    expectDownloadSequence('', null, true, false);
   });
 
   it('should try to load the latest FHIR R5 package from build.fhir.org when it is not locally cached', async () => {