Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance Template metadata #875

Merged
merged 4 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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