From 33532823481bea841d0bd70f928955783a66f267 Mon Sep 17 00:00:00 2001 From: Josh Spicer Date: Tue, 14 Nov 2023 22:16:13 +0000 Subject: [PATCH 1/5] programatically edit a Feature pinned version via upgrade cmd --- .../containerFeaturesConfiguration.ts | 4 +- src/spec-configuration/lockfile.ts | 33 ++--- src/spec-node/upgradeCommand.ts | 133 +++++++++++++++--- 3 files changed, 129 insertions(+), 41 deletions(-) diff --git a/src/spec-configuration/containerFeaturesConfiguration.ts b/src/spec-configuration/containerFeaturesConfiguration.ts index 24a65fb0f..7544a52ea 100644 --- a/src/spec-configuration/containerFeaturesConfiguration.ts +++ b/src/spec-configuration/containerFeaturesConfiguration.ts @@ -18,7 +18,7 @@ import { request } from '../spec-utils/httpRequest'; import { fetchOCIFeature, tryGetOCIFeatureSet, fetchOCIFeatureManifestIfExistsFromUserIdentifier } from './containerFeaturesOCI'; import { uriToFsPath } from './configurationCommonUtils'; import { CommonParams, ManifestContainer, OCIManifest, OCIRef, getRef, getVersionsStrictSorted } from './containerCollectionsOCI'; -import { Lockfile, readLockfile, writeLockfile } from './lockfile'; +import { Lockfile, generateLockfile, readLockfile, writeLockfile } from './lockfile'; import { computeDependsOnInstallationOrder } from './containerFeaturesOrder'; import { logFeatureAdvisories } from './featureAdvisories'; import { getEntPasswdShellCommand } from '../spec-common/commonUtils'; @@ -570,7 +570,7 @@ export async function generateFeaturesConfig(params: ContainerFeatureInternalPar await fetchFeatures(params, featuresConfig, locallyCachedFeatureSet, dstFolder, localFeaturesFolder, ociCacheDir, lockfile); await logFeatureAdvisories(params, featuresConfig); - await writeLockfile(params, config, featuresConfig, initLockfile); + await writeLockfile(params, config, await generateLockfile(featuresConfig), initLockfile); return featuresConfig; } diff --git a/src/spec-configuration/lockfile.ts b/src/spec-configuration/lockfile.ts index dc9d77ba9..9de0ba0b2 100644 --- a/src/spec-configuration/lockfile.ts +++ b/src/spec-configuration/lockfile.ts @@ -13,20 +13,8 @@ export interface Lockfile { features: Record; } -export async function writeLockfile(params: ContainerFeatureInternalParams, config: DevContainerConfig, featuresConfig: FeaturesConfig, forceInitLockfile?: boolean, dryRun?: boolean): Promise { - const lockfilePath = getLockfilePath(config); - const oldLockfileContent = await readLocalFile(lockfilePath) - .catch(err => { - if (err?.code !== 'ENOENT') { - throw err; - } - }); - - if (!forceInitLockfile && !oldLockfileContent && !params.experimentalLockfile && !params.experimentalFrozenLockfile) { - return; - } - - const lockfile: Lockfile = featuresConfig.featureSets +export async function generateLockfile(featuresConfig: FeaturesConfig): Promise { + return featuresConfig.featureSets .map(f => [f, f.sourceInformation] as const) .filter((tup): tup is [FeatureSet, OCISourceInformation | DirectTarballSourceInformation] => ['oci', 'direct-tarball'].indexOf(tup[1].type) !== -1) .map(([set, source]) => { @@ -50,13 +38,22 @@ export async function writeLockfile(params: ContainerFeatureInternalParams, conf }, { features: {} as Record, }); +} - const newLockfileContentString = JSON.stringify(lockfile, null, 2); +export async function writeLockfile(params: ContainerFeatureInternalParams, config: DevContainerConfig, lockfile: Lockfile, forceInitLockfile?: boolean): Promise { + const lockfilePath = getLockfilePath(config); + const oldLockfileContent = await readLocalFile(lockfilePath) + .catch(err => { + if (err?.code !== 'ENOENT') { + throw err; + } + }); - if (dryRun) { - return newLockfileContentString; + if (!forceInitLockfile && !oldLockfileContent && !params.experimentalLockfile && !params.experimentalFrozenLockfile) { + return; } + const newLockfileContentString = JSON.stringify(lockfile, null, 2); const newLockfileContent = Buffer.from(newLockfileContentString); if (params.experimentalFrozenLockfile && !oldLockfileContent) { throw new Error('Lockfile does not exist.'); @@ -73,7 +70,7 @@ export async function writeLockfile(params: ContainerFeatureInternalParams, conf export async function readLockfile(config: DevContainerConfig): Promise<{ lockfile?: Lockfile; initLockfile?: boolean }> { try { const content = await readLocalFile(getLockfilePath(config)); - // If empty file, use as maker to initialize lockfile when build completes. + // If empty file, use as marker to initialize lockfile when build completes. if (content.toString().trim() === '') { return { initLockfile: true }; } diff --git a/src/spec-node/upgradeCommand.ts b/src/spec-node/upgradeCommand.ts index cfbee0025..d28ecc4f6 100644 --- a/src/spec-node/upgradeCommand.ts +++ b/src/spec-node/upgradeCommand.ts @@ -1,3 +1,5 @@ +import * as jsonc from 'jsonc-parser'; + import { Argv } from 'yargs'; import { UnpackArgv } from './devContainersSpecCLI'; import { dockerComposeCLIConfig } from './dockerCompose'; @@ -6,17 +8,18 @@ import { createLog } from './devContainers'; import { getPackageConfig } from '../spec-utils/product'; import { DockerCLIParameters } from '../spec-shutdown/dockerUtils'; import path from 'path'; -import { getCLIHost } from '../spec-common/cliHost'; +import { CLIHost, getCLIHost } from '../spec-common/cliHost'; import { loadNativeModule } from '../spec-common/commonUtils'; import { URI } from 'vscode-uri'; -import { workspaceFromPath } from '../spec-utils/workspaces'; +import { Workspace, workspaceFromPath } from '../spec-utils/workspaces'; import { getDefaultDevContainerConfigPath, getDevContainerConfigPathIn, uriToFsPath } from '../spec-configuration/configurationCommonUtils'; import { readDevContainerConfigFile } from './configContainer'; import { ContainerError } from '../spec-common/errors'; import { getCacheFolder } from './utils'; -import { getLockfilePath, writeLockfile } from '../spec-configuration/lockfile'; -import { writeLocalFile } from '../spec-utils/pfs'; +import { Lockfile, generateLockfile, getLockfilePath, writeLockfile } from '../spec-configuration/lockfile'; +import { isLocalFile, readLocalFile, writeLocalFile } from '../spec-utils/pfs'; import { readFeaturesConfig } from './featureUtils'; +import { DevContainerConfig } from '../spec-configuration/configuration'; export function featuresUpgradeOptions(y: Argv) { return y @@ -27,6 +30,22 @@ export function featuresUpgradeOptions(y: Argv) { 'config': { type: 'string', description: 'devcontainer.json path. The default is to use .devcontainer/devcontainer.json or, if that does not exist, .devcontainer.json in the workspace folder.' }, 'log-level': { choices: ['error' as 'error', 'info' as 'info', 'debug' as 'debug', 'trace' as 'trace'], default: 'info' as 'info', description: 'Log level.' }, 'dry-run': { type: 'boolean', description: 'Write generated lockfile to standard out instead of to disk.' }, + // Added for dependabot + 'feature': { hidden: true, type: 'string', alias: 'f', description: 'Upgrade the version requirements of a given Feature (and its dependencies). Then, upgrade the lockfile. Must supply \'--target-version\'.' }, + 'target-version': { hidden: true, type: 'string', alias: 'v', description: 'The major (x), minor (x.y), or patch version (x.y.z) of the Feature to pin in devcontainer.json. Must supply a \'--feature\'.' }, + }) + .check(argv => { + if (argv.feature && !argv['target-version'] || !argv.feature && argv['target-version']) { + throw new Error('The \'--target-version\' and \'--feature\' flag must be used together.'); + } + + if (argv['target-version']) { + const targetVersion = argv['target-version']; + if (!targetVersion.match(/^\d+(\.\d+(\.\d+)?)?$/)) { + throw new Error(`Invalid version '${targetVersion}'. Must be in the form of 'x', 'x.y', or 'x.y.z'`); + } + } + return true; }); } @@ -43,6 +62,8 @@ async function featuresUpgrade({ 'docker-compose-path': dockerComposePath, 'log-level': inputLogLevel, 'dry-run': dryRun, + feature: feature, + 'target-version': targetVersion, }: FeaturesUpgradeArgs) { const disposables: (() => Promise | undefined)[] = []; const dispose = async () => { @@ -77,11 +98,7 @@ async function featuresUpgrade({ const workspace = workspaceFromPath(cliHost.path, workspaceFolder); const configPath = configFile ? configFile : await getDevContainerConfigPathIn(cliHost, workspace.configFolderPath); - const configs = configPath && await readDevContainerConfigFile(cliHost, workspace, configPath, true, output) || undefined; - if (!configs) { - throw new ContainerError({ description: `Dev container config (${uriToFsPath(configFile || getDefaultDevContainerConfigPath(cliHost, workspace!.configFolderPath), cliHost.platform)}) not found.` }); - } - const config = configs.config.config; + let config = await getConfig(configPath, cliHost, workspace, output, configFile); const cacheFolder = await getCacheFolder(cliHost); const params = { extensionPath, @@ -93,25 +110,31 @@ async function featuresUpgrade({ platform: cliHost.platform, }; - const bold = process.stdout.isTTY ? '\x1b[1m' : ''; - const clear = process.stdout.isTTY ? '\x1b[0m' : ''; - output.raw(`${bold}Upgrading lockfile...\n${clear}\n`, LogLevel.Info); + if (feature && targetVersion) { + output.write(`Updating '${feature}' to '${targetVersion}' in devcontainer.json`, LogLevel.Info); + // Update Feature version tag in devcontainer.json + await updateFeatureVersionInConfig(params, config, config.configFilePath!.fsPath, feature, targetVersion); + // Re-read config for subsequent lockfile generation + config = await getConfig(configPath, cliHost, workspace, output, configFile); + } - // Truncate existing lockfile - const lockfilePath = getLockfilePath(config); - await writeLocalFile(lockfilePath, ''); - // Update lockfile const featuresConfig = await readFeaturesConfig(dockerParams, pkg, config, extensionPath, false, {}); if (!featuresConfig) { throw new ContainerError({ description: `Failed to update lockfile` }); } - const lockFile = await writeLockfile(params, config, featuresConfig, true, dryRun); + + const lockfile: Lockfile = await generateLockfile(featuresConfig); + if (dryRun) { - if (!lockFile) { - throw new ContainerError({ description: `Failed to generate lockfile.` }); - } - console.log(lockFile); + console.log(JSON.stringify(lockfile, null, 2)); + return; } + + // Truncate any existing lockfile + const lockfilePath = getLockfilePath(config); + await writeLocalFile(lockfilePath, ''); + // Update lockfile + await writeLockfile(params, config, lockfile, true); } catch (err) { if (output) { output.write(err && (err.stack || err.message) || String(err)); @@ -123,4 +146,72 @@ async function featuresUpgrade({ } await dispose(); process.exit(0); +} + +async function updateFeatureVersionInConfig(params: { output: Log }, config: DevContainerConfig, configPath: string, targetFeature: string, targetVersion: string) { + const { output } = params; + + if (!config.features) { + // No Features in config to upgrade + output.write(`No Features found in '${configPath}'.`); + return; + } + + if (!configPath || !(await isLocalFile(configPath))) { + throw new ContainerError({ description: `Error running upgrade command. Config path '${configPath}' does not exist.` }); + } + + const configText = await readLocalFile(configPath); + const previousConfigText: string = configText.toString(); + let updatedText: string = configText.toString(); + + // Clear Features object to make editing easier + const edits = jsonc.modify(updatedText, ['features'], {}, { formattingOptions: {} }); + updatedText = jsonc.applyEdits(updatedText, edits); + + const targetFeatureNoVersion = getFeatureIdWithoutVersion(targetFeature); + for (const [userFeatureId, options] of Object.entries(config.features)) { + + // Filter to only modify target Feature. + // Rewrite non-target Feature back exactly how they were. + if (targetFeatureNoVersion !== getFeatureIdWithoutVersion(userFeatureId)) { + const propertyPath = ['features', userFeatureId]; + updatedText = applyEdit(updatedText, propertyPath, options ?? {}); + continue; + } + + const updatedId = `${getFeatureIdWithoutVersion(userFeatureId)}:${targetVersion}`; + + // Update config + const propertyPath = ['features', updatedId]; + updatedText = applyEdit(updatedText, propertyPath, options ?? {}); + } + + output.write(updatedText, LogLevel.Trace); + if (updatedText === previousConfigText) { + output.write(`No changes to config file: ${configPath}\n`, LogLevel.Trace); + return; + } + + output.write(`Updating config file: '${configPath}'`, LogLevel.Info); + await writeLocalFile(configPath, updatedText); +} + +function applyEdit(text: string, propertyPath: string[], options: string | boolean | Record): string { + let edits: jsonc.Edit[] = jsonc.modify(text, propertyPath, options, { formattingOptions: {} }); + return jsonc.applyEdits(text, edits); +} + +async function getConfig(configPath: URI | undefined, cliHost: CLIHost, workspace: Workspace, output: Log, configFile: URI | undefined): Promise { + const configs = configPath && await readDevContainerConfigFile(cliHost, workspace, configPath, true, output) || undefined; + if (!configs) { + throw new ContainerError({ description: `Dev container config (${uriToFsPath(configFile || getDefaultDevContainerConfigPath(cliHost, workspace!.configFolderPath), cliHost.platform)}) not found.` }); + } + return configs.config.config; +} + +const lastDelimiter = /[:@][^/]*$/; +function getFeatureIdWithoutVersion(featureId: string) { + const m = lastDelimiter.exec(featureId); + return m ? featureId.substring(0, m.index) : featureId; } \ No newline at end of file From 0ae5b3eae46c90b762e6c9a76dcdc4cd4490d17e Mon Sep 17 00:00:00 2001 From: Josh Spicer Date: Tue, 14 Nov 2023 23:00:56 +0000 Subject: [PATCH 2/5] add test --- .../lockfile-upgrade-feature/.gitignore | 2 ++ .../expected.devcontainer.json | 10 ++++++++++ .../input.devcontainer.json | 10 ++++++++++ src/test/container-features/lockfile.test.ts | 18 ++++++++++++++++++ 4 files changed, 40 insertions(+) create mode 100644 src/test/container-features/configs/lockfile-upgrade-feature/.gitignore create mode 100644 src/test/container-features/configs/lockfile-upgrade-feature/expected.devcontainer.json create mode 100644 src/test/container-features/configs/lockfile-upgrade-feature/input.devcontainer.json diff --git a/src/test/container-features/configs/lockfile-upgrade-feature/.gitignore b/src/test/container-features/configs/lockfile-upgrade-feature/.gitignore new file mode 100644 index 000000000..eb1a418e1 --- /dev/null +++ b/src/test/container-features/configs/lockfile-upgrade-feature/.gitignore @@ -0,0 +1,2 @@ +.devcontainer-lock.json +.devcontainer.json \ No newline at end of file diff --git a/src/test/container-features/configs/lockfile-upgrade-feature/expected.devcontainer.json b/src/test/container-features/configs/lockfile-upgrade-feature/expected.devcontainer.json new file mode 100644 index 000000000..3c4f6a3d1 --- /dev/null +++ b/src/test/container-features/configs/lockfile-upgrade-feature/expected.devcontainer.json @@ -0,0 +1,10 @@ +{ + "image": "mcr.microsoft.com/devcontainers/base", + // Comment + "features": { + "ghcr.io/codspace/versioning/bar:1.0.0": {}, + "ghcr.io/codspace/versioning/foo:2": { + "hello": "world" + } + } +} \ No newline at end of file diff --git a/src/test/container-features/configs/lockfile-upgrade-feature/input.devcontainer.json b/src/test/container-features/configs/lockfile-upgrade-feature/input.devcontainer.json new file mode 100644 index 000000000..248513127 --- /dev/null +++ b/src/test/container-features/configs/lockfile-upgrade-feature/input.devcontainer.json @@ -0,0 +1,10 @@ +{ + "image": "mcr.microsoft.com/devcontainers/base", + // Comment + "features": { + "ghcr.io/codspace/versioning/bar:1.0.0": {}, + "ghcr.io/codspace/versioning/foo:1": { + "hello": "world" + } + } +} \ No newline at end of file diff --git a/src/test/container-features/lockfile.test.ts b/src/test/container-features/lockfile.test.ts index 85ef93750..b09500881 100644 --- a/src/test/container-features/lockfile.test.ts +++ b/src/test/container-features/lockfile.test.ts @@ -146,6 +146,24 @@ describe('Lockfile', function () { assert.ok(lockfile.features['ghcr.io/codspace/dependson/A:2']); }); + it('upgrade command with --feature', async () => { + const workspaceFolder = path.join(__dirname, 'configs/lockfile-upgrade-feature'); + await cpLocal(path.join(workspaceFolder, 'input.devcontainer.json'), path.join(workspaceFolder, '.devcontainer.json')); + + const res = await shellExec(`${cli} upgrade --dry-run --workspace-folder ${workspaceFolder} --feature ghcr.io/codspace/versioning/foo --target-version 2`); + + // Check devcontainer.json was updated + const actual = await readLocalFile(path.join(workspaceFolder, '.devcontainer.json')); + const expected = await readLocalFile(path.join(workspaceFolder, 'expected.devcontainer.json')); + assert.equal(actual.toString(), expected.toString()); + + // Check lockfile was updated + const lockfile = JSON.parse(res.stdout); + assert.ok(lockfile); + assert.ok(lockfile.features); + assert.ok(lockfile.features['ghcr.io/codspace/versioning/foo:2'].version === '2.11.1'); + }); + it('OCI feature integrity', async () => { const workspaceFolder = path.join(__dirname, 'configs/lockfile-oci-integrity'); From 4d37cb7c3871c43303353f503217fdd12bea5f3d Mon Sep 17 00:00:00 2001 From: Josh Spicer Date: Mon, 20 Nov 2023 23:13:19 +0000 Subject: [PATCH 3/5] dont remove all features during upgrade --- src/spec-node/upgradeCommand.ts | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/src/spec-node/upgradeCommand.ts b/src/spec-node/upgradeCommand.ts index d28ecc4f6..8ddae183e 100644 --- a/src/spec-node/upgradeCommand.ts +++ b/src/spec-node/upgradeCommand.ts @@ -165,26 +165,16 @@ async function updateFeatureVersionInConfig(params: { output: Log }, config: Dev const previousConfigText: string = configText.toString(); let updatedText: string = configText.toString(); - // Clear Features object to make editing easier - const edits = jsonc.modify(updatedText, ['features'], {}, { formattingOptions: {} }); - updatedText = jsonc.applyEdits(updatedText, edits); - const targetFeatureNoVersion = getFeatureIdWithoutVersion(targetFeature); for (const [userFeatureId, options] of Object.entries(config.features)) { - - // Filter to only modify target Feature. - // Rewrite non-target Feature back exactly how they were. if (targetFeatureNoVersion !== getFeatureIdWithoutVersion(userFeatureId)) { - const propertyPath = ['features', userFeatureId]; - updatedText = applyEdit(updatedText, propertyPath, options ?? {}); continue; } - const updatedId = `${getFeatureIdWithoutVersion(userFeatureId)}:${targetVersion}`; - - // Update config - const propertyPath = ['features', updatedId]; - updatedText = applyEdit(updatedText, propertyPath, options ?? {}); + // Remove existing Feature and replace with Feature with updated version tag + const currentFeaturePath = ['features', userFeatureId]; + const updatedFeaturePath = ['features', `${targetFeatureNoVersion}:${targetVersion}`]; + updatedText = upgradeFeatureKeyInConfig(updatedText, currentFeaturePath, updatedFeaturePath, options); } output.write(updatedText, LogLevel.Trace); @@ -197,9 +187,9 @@ async function updateFeatureVersionInConfig(params: { output: Log }, config: Dev await writeLocalFile(configPath, updatedText); } -function applyEdit(text: string, propertyPath: string[], options: string | boolean | Record): string { - let edits: jsonc.Edit[] = jsonc.modify(text, propertyPath, options, { formattingOptions: {} }); - return jsonc.applyEdits(text, edits); +function upgradeFeatureKeyInConfig(text: string, currentPropertyPath: string[], updatedPropertyPath: string[], options: string | boolean | Record): string { + text = jsonc.applyEdits(text, jsonc.modify(text, currentPropertyPath, undefined, { formattingOptions: {} })); // Remove existing Feature + return jsonc.applyEdits(text, jsonc.modify(text, updatedPropertyPath, options, { formattingOptions: {} })); } async function getConfig(configPath: URI | undefined, cliHost: CLIHost, workspace: Workspace, output: Log, configFile: URI | undefined): Promise { From c13b2c228899026829c9e6ca7384f9d9db856d15 Mon Sep 17 00:00:00 2001 From: Josh Spicer Date: Mon, 20 Nov 2023 23:50:02 +0000 Subject: [PATCH 4/5] use regex to replace --- src/spec-node/upgradeCommand.ts | 15 ++++++--------- .../expected.devcontainer.json | 6 ++++-- .../input.devcontainer.json | 6 ++++-- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/spec-node/upgradeCommand.ts b/src/spec-node/upgradeCommand.ts index 8ddae183e..4315d0cb2 100644 --- a/src/spec-node/upgradeCommand.ts +++ b/src/spec-node/upgradeCommand.ts @@ -166,15 +166,12 @@ async function updateFeatureVersionInConfig(params: { output: Log }, config: Dev let updatedText: string = configText.toString(); const targetFeatureNoVersion = getFeatureIdWithoutVersion(targetFeature); - for (const [userFeatureId, options] of Object.entries(config.features)) { + for (const [userFeatureId, _] of Object.entries(config.features)) { if (targetFeatureNoVersion !== getFeatureIdWithoutVersion(userFeatureId)) { continue; } - - // Remove existing Feature and replace with Feature with updated version tag - const currentFeaturePath = ['features', userFeatureId]; - const updatedFeaturePath = ['features', `${targetFeatureNoVersion}:${targetVersion}`]; - updatedText = upgradeFeatureKeyInConfig(updatedText, currentFeaturePath, updatedFeaturePath, options); + updatedText = upgradeFeatureKeyInConfig(updatedText, userFeatureId, `${targetFeatureNoVersion}:${targetVersion}`); + break; } output.write(updatedText, LogLevel.Trace); @@ -187,9 +184,9 @@ async function updateFeatureVersionInConfig(params: { output: Log }, config: Dev await writeLocalFile(configPath, updatedText); } -function upgradeFeatureKeyInConfig(text: string, currentPropertyPath: string[], updatedPropertyPath: string[], options: string | boolean | Record): string { - text = jsonc.applyEdits(text, jsonc.modify(text, currentPropertyPath, undefined, { formattingOptions: {} })); // Remove existing Feature - return jsonc.applyEdits(text, jsonc.modify(text, updatedPropertyPath, options, { formattingOptions: {} })); +function upgradeFeatureKeyInConfig(configText: string, current: string, updated: string) { + const featureIdRegex = new RegExp(current, 'g'); + return configText.replace(featureIdRegex, updated); } async function getConfig(configPath: URI | undefined, cliHost: CLIHost, workspace: Workspace, output: Log, configFile: URI | undefined): Promise { diff --git a/src/test/container-features/configs/lockfile-upgrade-feature/expected.devcontainer.json b/src/test/container-features/configs/lockfile-upgrade-feature/expected.devcontainer.json index 3c4f6a3d1..6cfcf5ea6 100644 --- a/src/test/container-features/configs/lockfile-upgrade-feature/expected.devcontainer.json +++ b/src/test/container-features/configs/lockfile-upgrade-feature/expected.devcontainer.json @@ -3,8 +3,10 @@ // Comment "features": { "ghcr.io/codspace/versioning/bar:1.0.0": {}, - "ghcr.io/codspace/versioning/foo:2": { - "hello": "world" + // Comment + "ghcr.io/codspace/versioning/foo:2": { // Comment + "hello": "world" // Comment } + // Comment } } \ No newline at end of file diff --git a/src/test/container-features/configs/lockfile-upgrade-feature/input.devcontainer.json b/src/test/container-features/configs/lockfile-upgrade-feature/input.devcontainer.json index 248513127..8b82552b0 100644 --- a/src/test/container-features/configs/lockfile-upgrade-feature/input.devcontainer.json +++ b/src/test/container-features/configs/lockfile-upgrade-feature/input.devcontainer.json @@ -3,8 +3,10 @@ // Comment "features": { "ghcr.io/codspace/versioning/bar:1.0.0": {}, - "ghcr.io/codspace/versioning/foo:1": { - "hello": "world" + // Comment + "ghcr.io/codspace/versioning/foo:1": { // Comment + "hello": "world" // Comment } + // Comment } } \ No newline at end of file From 13763df5a6cbea31feb87b21e0b9c38561a69ef4 Mon Sep 17 00:00:00 2001 From: Josh Spicer Date: Mon, 20 Nov 2023 23:51:38 +0000 Subject: [PATCH 5/5] typecheck --- src/spec-node/upgradeCommand.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/spec-node/upgradeCommand.ts b/src/spec-node/upgradeCommand.ts index 4315d0cb2..d3524b02f 100644 --- a/src/spec-node/upgradeCommand.ts +++ b/src/spec-node/upgradeCommand.ts @@ -1,5 +1,3 @@ -import * as jsonc from 'jsonc-parser'; - import { Argv } from 'yargs'; import { UnpackArgv } from './devContainersSpecCLI'; import { dockerComposeCLIConfig } from './dockerCompose';