Skip to content

Commit

Permalink
Enhance Template metadata (#875)
Browse files Browse the repository at this point in the history
* update Template interface

* update Template metadata to include files. Fix featureIds by calling getRef(...)

* improve test

* replace metadata entry for optionalPaths to full file path when only one file in target dir
  • Loading branch information
joshspicer authored Aug 22, 2024
1 parent fbd8dfe commit b01622e
Show file tree
Hide file tree
Showing 17 changed files with 201 additions and 5 deletions.
6 changes: 4 additions & 2 deletions src/spec-configuration/containerTemplatesConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, TemplateOption>;
platforms?: string[];
publisher?: string;
keywords?: string[];
optionalPaths?: string[];
files: string[]; // Added programatically during packaging
}

export type TemplateOption = {
Expand Down
41 changes: 39 additions & 2 deletions src/spec-node/collectionCommonUtils/packageCommandImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));

Expand Down
Original file line number Diff line number Diff line change
@@ -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": {}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "devcontainers" # See documentation for possible values
directory: "/"
schedule:
interval: weekly
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hello
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hi
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export class C1 {
constructor() {
// Add your code here
}

// Add your methods here
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export class C2 {
constructor() {
// Add your code here
}

// Add your methods here
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export class C3 {
constructor() {
// Add your code here
}

// Add your methods here
}
Original file line number Diff line number Diff line change
@@ -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"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "devcontainers" # See documentation for possible values
directory: "/"
schedule:
interval: weekly
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export class A1 {
constructor() {
// Add your code here
}

// Add your methods here
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export class A2 {
constructor() {
// Add your code here
}

// Add your methods here
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "devcontainers" # See documentation for possible values
directory: "/"
schedule:
interval: weekly
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export class B2 {
constructor() {
// Add your code here
}

// Add your methods here
}
42 changes: 41 additions & 1 deletion src/test/container-templates/templatesCLICommands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 () {
Expand Down

0 comments on commit b01622e

Please sign in to comment.