diff --git a/src/spec-configuration/containerTemplatesConfiguration.ts b/src/spec-configuration/containerTemplatesConfiguration.ts index 3f64507a8..2020bac65 100644 --- a/src/spec-configuration/containerTemplatesConfiguration.ts +++ b/src/spec-configuration/containerTemplatesConfiguration.ts @@ -5,13 +5,15 @@ export interface Template { description?: string; documentationURL?: string; licenseURL?: string; - type?: string; - fileCount?: number; + type?: string; // Added programatically during packaging + fileCount?: number; // Added programatically during packaging featureIds?: string[]; options?: Record; platforms?: string[]; publisher?: string; keywords?: string[]; + optionalPaths?: string[]; + files: string[]; // Added programatically during packaging } export type TemplateOption = { diff --git a/src/spec-node/collectionCommonUtils/packageCommandImpl.ts b/src/spec-node/collectionCommonUtils/packageCommandImpl.ts index 2be6ace02..271c0a049 100644 --- a/src/spec-node/collectionCommonUtils/packageCommandImpl.ts +++ b/src/spec-node/collectionCommonUtils/packageCommandImpl.ts @@ -9,6 +9,7 @@ import path from 'path'; import { DevContainerConfig, isDockerFileConfig } from '../../spec-configuration/configuration'; import { Template } from '../../spec-configuration/containerTemplatesConfiguration'; import { Feature } from '../../spec-configuration/containerFeaturesConfiguration'; +import { getRef } from '../../spec-configuration/containerCollectionsOCI'; export interface SourceInformation { source: string; @@ -133,9 +134,45 @@ async function addsAdditionalTemplateProps(srcFolder: string, devcontainerTempla return false; } + const fileNames = (await recursiveDirReader.default(srcFolder))?.map((f) => path.relative(srcFolder, f)) ?? []; + templateData.type = type; - templateData.fileCount = (await recursiveDirReader.default(srcFolder)).length; - templateData.featureIds = config.features ? Object.keys(config.features).map((k) => k.split(':')[0]) : []; + templateData.files = fileNames; + templateData.fileCount = fileNames.length; + templateData.featureIds = + config.features + ? Object.keys(config.features) + .map((f) => getRef(output, f)?.resource) + .filter((f) => f !== undefined) as string[] + : []; + + // If the Template is omitting a folder and that folder contains just a single file, + // replace the entry in the metadata with the full file name, + // as that provides a better user experience when tools consume the metadata. + // Eg: If the template is omitting ".github/*" and the Template source contains just a single file + // "workflow.yml", replace ".github/*" with ".github/workflow.yml" + if (templateData.optionalPaths && templateData.optionalPaths?.length) { + const optionalPaths = templateData.optionalPaths; + for (const optPath of optionalPaths) { + // Skip if not a directory + if (!optPath.endsWith('/*') || optPath.length < 3) { + continue; + } + const dirPath = optPath.slice(0, -2); + const dirFiles = fileNames.filter((f) => f.startsWith(dirPath)); + output.write(`Given optionalPath starting with '${dirPath}' has ${dirFiles.length} files`, LogLevel.Trace); + if (dirFiles.length === 1) { + // If that one item is a file and not a directory + const f = dirFiles[0]; + output.write(`Checking if optionalPath '${optPath}' with lone contents '${f}' is a file `, LogLevel.Trace); + const localPath = path.join(srcFolder, f); + if (await isLocalFile(localPath)) { + output.write(`Checked path '${localPath}' on disk is a file. Replacing optionalPaths entry '${optPath}' with '${f}'`, LogLevel.Trace); + templateData.optionalPaths[optionalPaths.indexOf(optPath)] = f; + } + } + } + } await writeLocalFile(devcontainerTemplateJsonPath, JSON.stringify(templateData, null, 4)); diff --git a/src/test/container-templates/example-templates-sets/simple/src/mytemplate/.devcontainer/devcontainer.json b/src/test/container-templates/example-templates-sets/simple/src/mytemplate/.devcontainer/devcontainer.json new file mode 100644 index 000000000..22cd2e51a --- /dev/null +++ b/src/test/container-templates/example-templates-sets/simple/src/mytemplate/.devcontainer/devcontainer.json @@ -0,0 +1,16 @@ +{ + "image": "mcr.microsoft.com/devcontainers/base", + "containerEnv": { + "MY_ENV_VAR": "${templateOption:anOption}" + }, + "features": { + "ghcr.io/devcontainers/features/azure-cli": { + "installBicep": false + }, + "ghcr.io/devcontainers/features/aws-cli:1": {}, + "ghcr.io/devcontainers/features/common-utils:2.5.1": { + "userUid": "${templateOption:userUid}" + }, + "ghcr.io/devcontainers/features/docker-in-docker@sha256:503f23cd692325b3cbb8c20a0ecfabb3444b0c786b363e0c82572bd7d71dc099": {} + } +} \ No newline at end of file diff --git a/src/test/container-templates/example-templates-sets/simple/src/mytemplate/.github/dependabot.yml b/src/test/container-templates/example-templates-sets/simple/src/mytemplate/.github/dependabot.yml new file mode 100644 index 000000000..5c42e9d82 --- /dev/null +++ b/src/test/container-templates/example-templates-sets/simple/src/mytemplate/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "devcontainers" # See documentation for possible values + directory: "/" + schedule: + interval: weekly \ No newline at end of file diff --git a/src/test/container-templates/example-templates-sets/simple/src/mytemplate/assets/hello.md b/src/test/container-templates/example-templates-sets/simple/src/mytemplate/assets/hello.md new file mode 100644 index 000000000..5ab2f8a43 --- /dev/null +++ b/src/test/container-templates/example-templates-sets/simple/src/mytemplate/assets/hello.md @@ -0,0 +1 @@ +Hello \ No newline at end of file diff --git a/src/test/container-templates/example-templates-sets/simple/src/mytemplate/assets/hi.md b/src/test/container-templates/example-templates-sets/simple/src/mytemplate/assets/hi.md new file mode 100644 index 000000000..40816a2b5 --- /dev/null +++ b/src/test/container-templates/example-templates-sets/simple/src/mytemplate/assets/hi.md @@ -0,0 +1 @@ +Hi \ No newline at end of file diff --git a/src/test/container-templates/example-templates-sets/simple/src/mytemplate/c1.ts b/src/test/container-templates/example-templates-sets/simple/src/mytemplate/c1.ts new file mode 100644 index 000000000..fd59b8571 --- /dev/null +++ b/src/test/container-templates/example-templates-sets/simple/src/mytemplate/c1.ts @@ -0,0 +1,7 @@ +export class C1 { + constructor() { + // Add your code here + } + + // Add your methods here +} \ No newline at end of file diff --git a/src/test/container-templates/example-templates-sets/simple/src/mytemplate/c2.ts b/src/test/container-templates/example-templates-sets/simple/src/mytemplate/c2.ts new file mode 100644 index 000000000..b0e0d3d87 --- /dev/null +++ b/src/test/container-templates/example-templates-sets/simple/src/mytemplate/c2.ts @@ -0,0 +1,7 @@ +export class C2 { + constructor() { + // Add your code here + } + + // Add your methods here +} \ No newline at end of file diff --git a/src/test/container-templates/example-templates-sets/simple/src/mytemplate/c3.ts b/src/test/container-templates/example-templates-sets/simple/src/mytemplate/c3.ts new file mode 100644 index 000000000..d6c04bf63 --- /dev/null +++ b/src/test/container-templates/example-templates-sets/simple/src/mytemplate/c3.ts @@ -0,0 +1,7 @@ +export class C3 { + constructor() { + // Add your code here + } + + // Add your methods here +} \ No newline at end of file diff --git a/src/test/container-templates/example-templates-sets/simple/src/mytemplate/devcontainer-template.json b/src/test/container-templates/example-templates-sets/simple/src/mytemplate/devcontainer-template.json new file mode 100644 index 000000000..dbcf808b5 --- /dev/null +++ b/src/test/container-templates/example-templates-sets/simple/src/mytemplate/devcontainer-template.json @@ -0,0 +1,39 @@ +{ + "id": "mytemplate", + "version": "1.0.0", + "name": "My Template", + "description": "Simple test", + "documentationURL": "https://github.com/codspace/templates/tree/main/src/test", + "publisher": "codspace", + "licenseURL": "https://github.com/devcontainers/templates/blob/main/LICENSE", + "platforms": [ + "Any" + ], + "options": { + "anOption": { + "type": "string", + "description": "A great option", + "proposals": [ + "8.0", + "7.0", + "6.0" + ], + "default": "8.0" + }, + "userUid": { + "type": "string", + "description": "The user's UID", + "proposals": [ + "1000", + "1001", + "1002" + ], + "default": "1000" + } + }, + "optionalPaths": [ + ".github/*", + "example-projects/exampleA/*", + "c1.ts" + ] +} \ No newline at end of file diff --git a/src/test/container-templates/example-templates-sets/simple/src/mytemplate/example-projects/exampleA/.github/dependabot.yml b/src/test/container-templates/example-templates-sets/simple/src/mytemplate/example-projects/exampleA/.github/dependabot.yml new file mode 100644 index 000000000..5c42e9d82 --- /dev/null +++ b/src/test/container-templates/example-templates-sets/simple/src/mytemplate/example-projects/exampleA/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "devcontainers" # See documentation for possible values + directory: "/" + schedule: + interval: weekly \ No newline at end of file diff --git a/src/test/container-templates/example-templates-sets/simple/src/mytemplate/example-projects/exampleA/a1.ts b/src/test/container-templates/example-templates-sets/simple/src/mytemplate/example-projects/exampleA/a1.ts new file mode 100644 index 000000000..aa0531883 --- /dev/null +++ b/src/test/container-templates/example-templates-sets/simple/src/mytemplate/example-projects/exampleA/a1.ts @@ -0,0 +1,7 @@ +export class A1 { + constructor() { + // Add your code here + } + + // Add your methods here +} \ No newline at end of file diff --git a/src/test/container-templates/example-templates-sets/simple/src/mytemplate/example-projects/exampleA/subFolderA/a2.ts b/src/test/container-templates/example-templates-sets/simple/src/mytemplate/example-projects/exampleA/subFolderA/a2.ts new file mode 100644 index 000000000..4d279c27a --- /dev/null +++ b/src/test/container-templates/example-templates-sets/simple/src/mytemplate/example-projects/exampleA/subFolderA/a2.ts @@ -0,0 +1,7 @@ +export class A2 { + constructor() { + // Add your code here + } + + // Add your methods here +} \ No newline at end of file diff --git a/src/test/container-templates/example-templates-sets/simple/src/mytemplate/example-projects/exampleB/.github/dependabot.yml b/src/test/container-templates/example-templates-sets/simple/src/mytemplate/example-projects/exampleB/.github/dependabot.yml new file mode 100644 index 000000000..5c42e9d82 --- /dev/null +++ b/src/test/container-templates/example-templates-sets/simple/src/mytemplate/example-projects/exampleB/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "devcontainers" # See documentation for possible values + directory: "/" + schedule: + interval: weekly \ No newline at end of file diff --git a/src/test/container-templates/example-templates-sets/simple/src/mytemplate/example-projects/exampleB/b1.ts b/src/test/container-templates/example-templates-sets/simple/src/mytemplate/example-projects/exampleB/b1.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/test/container-templates/example-templates-sets/simple/src/mytemplate/example-projects/exampleB/subFolderB/b2.ts b/src/test/container-templates/example-templates-sets/simple/src/mytemplate/example-projects/exampleB/subFolderB/b2.ts new file mode 100644 index 000000000..810eb27a1 --- /dev/null +++ b/src/test/container-templates/example-templates-sets/simple/src/mytemplate/example-projects/exampleB/subFolderB/b2.ts @@ -0,0 +1,7 @@ +export class B2 { + constructor() { + // Add your code here + } + + // Add your methods here +} \ No newline at end of file diff --git a/src/test/container-templates/templatesCLICommands.test.ts b/src/test/container-templates/templatesCLICommands.test.ts index 1d3b86a8f..babb4f8f2 100644 --- a/src/test/container-templates/templatesCLICommands.test.ts +++ b/src/test/container-templates/templatesCLICommands.test.ts @@ -116,7 +116,7 @@ describe('tests packageTemplates()', async function () { const collectionFileExists = await isLocalFile(`${outputDir}/devcontainer-collection.json`); const json: DevContainerCollectionMetadata = JSON.parse((await readLocalFile(`${outputDir}/devcontainer-collection.json`)).toString()); - assert.strictEqual(json.templates.length, 3); + assert.strictEqual(json.templates.length, 4); assert.isTrue(collectionFileExists); // Checks if the automatically added properties are set correctly. @@ -140,6 +140,46 @@ describe('tests packageTemplates()', async function () { assert.equal(nodeProperties?.featureIds?.length, 2); assert.isTrue(nodeProperties?.featureIds?.some(f => f === 'ghcr.io/devcontainers/features/common-utils')); assert.isTrue(nodeProperties?.featureIds?.some(f => f === 'ghcr.io/devcontainers/features/git')); + + const mytemplateProperties: Template | undefined = json?.templates.find(t => t.id === 'mytemplate'); + console.log(JSON.stringify(mytemplateProperties, null, 4)); + assert.isNotEmpty(mytemplateProperties); + // -- optionalPaths + assert.strictEqual(mytemplateProperties?.optionalPaths?.length, 3); + assert.deepEqual(mytemplateProperties?.optionalPaths, + [ + '.github/dependabot.yml', // NOTE: Packaging step replaces the original value '.github/*' here since there's only a single file in the folder + 'example-projects/exampleA/*', + 'c1.ts' + ]); + // -- files + assert.strictEqual(mytemplateProperties?.files?.length, 14); + assert.deepEqual(mytemplateProperties?.files.sort(), [ + 'c1.ts', + 'c2.ts', + 'c3.ts', + 'devcontainer-template.json', + '.devcontainer/devcontainer.json', + '.github/dependabot.yml', + 'assets/hello.md', + 'assets/hi.md', + 'example-projects/exampleA/a1.ts', + 'example-projects/exampleA/.github/dependabot.yml', + 'example-projects/exampleA/subFolderA/a2.ts', + 'example-projects/exampleB/b1.ts', + 'example-projects/exampleB/.github/dependabot.yml', + 'example-projects/exampleB/subFolderB/b2.ts' + ].sort()); // Order isn't guaranteed + // -- featureIds + assert.strictEqual(mytemplateProperties?.featureIds?.length, 4); + assert.deepEqual(mytemplateProperties?.featureIds, [ + 'ghcr.io/devcontainers/features/azure-cli', + 'ghcr.io/devcontainers/features/aws-cli', + 'ghcr.io/devcontainers/features/common-utils', + 'ghcr.io/devcontainers/features/docker-in-docker' + ]); + + }); it('tests packaging for single template', async function () {