diff --git a/CHANGELOG.md b/CHANGELOG.md index a047d330e..5eca57472 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ Notable changes. +## June 2025 + +### [0.78.0] +- Fix: Handle missing features (https://github.com/devcontainers/cli/pull/1040) + ## May 2025 ### [0.77.0] diff --git a/package.json b/package.json index 3af1ba169..615afcb67 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@devcontainers/cli", "description": "Dev Containers CLI", - "version": "0.77.0", + "version": "0.78.0", "bin": { "devcontainer": "devcontainer.js" }, diff --git a/src/spec-configuration/containerFeaturesConfiguration.ts b/src/spec-configuration/containerFeaturesConfiguration.ts index 18c20309d..8309fb6ab 100644 --- a/src/spec-configuration/containerFeaturesConfiguration.ts +++ b/src/spec-configuration/containerFeaturesConfiguration.ts @@ -527,7 +527,7 @@ export async function loadVersionInfo(params: ContainerFeatureInternalParams, co const featureRef = getRef(nullLog, userFeatureId); // Filters out Feature identifiers that cannot be versioned (e.g. local paths, deprecated, etc..) if (featureRef) { const versions = (await getVersionsStrictSorted(params, featureRef)) - ?.reverse(); + ?.reverse() || []; if (versions) { const lockfileVersion = lockfile?.features[userFeatureId]?.version; let wanted = lockfileVersion; @@ -550,7 +550,7 @@ export async function loadVersionInfo(params: ContainerFeatureInternalParams, co wanted, wantedMajor: wanted && semver.major(wanted)?.toString(), latest: versions[0], - latestMajor: semver.major(versions[0])?.toString(), + latestMajor: versions[0] && semver.major(versions[0])?.toString(), }; } } diff --git a/src/test/container-features/configs/lockfile-outdated-command/.devcontainer.json b/src/test/container-features/configs/lockfile-outdated-command/.devcontainer.json index 7aba15391..aedc881e2 100644 --- a/src/test/container-features/configs/lockfile-outdated-command/.devcontainer.json +++ b/src/test/container-features/configs/lockfile-outdated-command/.devcontainer.json @@ -7,6 +7,7 @@ "ghcr.io/devcontainers/features/github-cli": "latest", "ghcr.io/devcontainers/features/azure-cli:0": "latest", "ghcr.io/codspace/versioning/foo:0.3.1": "latest", + "ghcr.io/codspace/doesnotexist:0.1.2": "latest", "./mylocalfeature": {}, "terraform": "latest", "https://myfeatures.com/features.tgz": "latest" diff --git a/src/test/container-features/lockfile.test.ts b/src/test/container-features/lockfile.test.ts index d0889fed8..57034e6f2 100644 --- a/src/test/container-features/lockfile.test.ts +++ b/src/test/container-features/lockfile.test.ts @@ -123,6 +123,14 @@ describe('Lockfile', function () { assert.strictEqual(foo.wantedMajor, '0'); assert.strictEqual(foo.latest, '2.11.1'); assert.strictEqual(foo.latestMajor, '2'); + + const doesnotexist = response.features['ghcr.io/codspace/doesnotexist:0.1.2']; + assert.ok(doesnotexist); + assert.strictEqual(doesnotexist.current, undefined); + assert.strictEqual(doesnotexist.wanted, undefined); + assert.strictEqual(doesnotexist.wantedMajor, undefined); + assert.strictEqual(doesnotexist.latest, undefined); + assert.strictEqual(doesnotexist.latestMajor, undefined); }); it('outdated command with text output', async () => { @@ -131,7 +139,7 @@ describe('Lockfile', function () { const res = await shellExec(`${cli} outdated --workspace-folder ${workspaceFolder} --output-format text`); const response = res.stdout; // Count number of lines of output - assert.strictEqual(response.split('\n').length, 7); // 5 valid Features + header + empty line + assert.strictEqual(response.split('\n').length, 8); // 5 valid Features + header + empty line // Check that the header is present assert.ok(response.includes('Current'), 'Current column is missing'); @@ -145,6 +153,7 @@ describe('Lockfile', function () { assert.ok(response.includes('ghcr.io/devcontainers/features/github-cli'), 'github-cli Feature is missing'); assert.ok(response.includes('ghcr.io/devcontainers/features/azure-cli'), 'azure-cli Feature is missing'); assert.ok(response.includes('ghcr.io/codspace/versioning/foo'), 'foo Feature is missing'); + assert.ok(response.includes('ghcr.io/codspace/doesnotexist'), 'doesnotexist Feature is missing'); // Check that filtered Features are not present assert.ok(!response.includes('mylocalfeature'));