Unverified Commit 542963cf authored by Samruddhi Khandale's avatar Samruddhi Khandale Committed by GitHub

Prep: republish features to ghcr.io (#78)

* syncing action

* modify release.yaml

* version update ; remove "install"

* add 'latest'

* add workflow condition
parent 69d3df5f
...@@ -22,15 +22,33 @@ inputs: ...@@ -22,15 +22,33 @@ inputs:
# 'features' options # 'features' options
base-path-to-features: base-path-to-features:
required: false required: false
default: './features/src' default: ''
description: "Relative path to the 'src' folder containing dev container 'feature(s)'" description: "Relative path to the 'src' folder containing dev container 'feature(s)'"
# 'template' options # 'template' options
base-path-to-templates: base-path-to-templates:
required: false required: false
default: './templates/src' default: ''
description: "Relative path to the folder containing dev container 'template(s)'" description: "Relative path to the folder containing dev container 'template(s)'"
# EXPERIMENTAL
tag-individual-features:
required: false
default: "false"
description: "Tag individual features"
publish-to-npm:
required: false
default: "false"
description: "Should publish features to NPM?"
publish-release-artifacts:
required: false
default: "false"
description: "Publish release artifacts (classic)"
publish-to-oci:
required: false
default: "false"
description: "Publish to OCI?"
runs: runs:
using: 'node16' using: 'node16'
main: 'dist/index.js' main: 'dist/index.js'
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -520,7 +520,7 @@ minipass ...@@ -520,7 +520,7 @@ minipass
ISC ISC
The ISC License The ISC License
Copyright (c) npm, Inc. and Contributors Copyright (c) 2017-2022 npm, Inc., Isaac Z. Schlueter, and Contributors
Permission to use, copy, modify, and/or distribute this software for any Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above purpose with or without fee is hereby granted, provided that the above
......
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
...@@ -32,95 +32,21 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge ...@@ -32,95 +32,21 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
}); });
}; };
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.generateFeaturesDocumentation = void 0; exports.generateTemplateDocumentation = exports.generateFeaturesDocumentation = void 0;
const fs = __importStar(require("fs")); const fs = __importStar(require("fs"));
const github = __importStar(require("@actions/github"));
const core = __importStar(require("@actions/core")); const core = __importStar(require("@actions/core"));
const path = __importStar(require("path")); const path = __importStar(require("path"));
function generateFeaturesDocumentation(basePath) { const utils_1 = require("./utils");
return __awaiter(this, void 0, void 0, function* () { const FEATURES_README_TEMPLATE = `
fs.readdir(basePath, (err, files) => { # #{Name}
if (err) {
core.error(err.message);
core.setFailed(`failed to generate 'features' documentation ${err.message}`);
return;
}
files.forEach(f => {
core.info(`Generating docs for feature '${f}'`);
if (f !== '.' && f !== '..') {
const readmePath = path.join(basePath, f, 'README.md');
// Reads in feature.json
const featureJsonPath = path.join(basePath, f, 'devcontainer-feature.json');
if (!fs.existsSync(featureJsonPath)) {
core.error(`devcontainer-feature.json not found at path '${featureJsonPath}'`);
return;
}
let featureJson = undefined;
try {
featureJson = JSON.parse(fs.readFileSync(featureJsonPath, 'utf8'));
}
catch (err) {
core.error(`Failed to parse ${featureJsonPath}: ${err}`);
return;
}
if (!featureJson || !(featureJson === null || featureJson === void 0 ? void 0 : featureJson.id)) {
core.error(`devcontainer-feature.json for feature '${f}' does not contain an 'id'`);
return;
}
const ref = github.context.ref;
const owner = github.context.repo.owner;
const repo = github.context.repo.repo;
// Add tag if parseable
let versionTag = 'latest';
if (ref.includes('refs/tags/')) {
versionTag = ref.replace('refs/tags/', '');
}
const generateOptionsMarkdown = () => {
const options = featureJson === null || featureJson === void 0 ? void 0 : featureJson.options;
if (!options) {
return '';
}
const keys = Object.keys(options);
const contents = keys
.map(k => {
const val = options[k];
return `| ${k} | ${val.description || '-'} | ${val.type || '-'} | ${val.default || '-'} |`;
})
.join('\n');
return ('| Options Id | Description | Type | Default Value |\n' +
'|-----|-----|-----|-----|\n' +
contents);
};
const newReadme = README_TEMPLATE.replace('#{nwo}', `${owner}/${repo}`)
.replace('#{versionTag}', versionTag)
.replace('#{featureId}', featureJson.id)
.replace('#{featureName}', featureJson.name
? `${featureJson.name} (${featureJson.id})`
: `${featureJson.id}`)
.replace('#{featureDescription}', featureJson.description ? featureJson.description : '')
.replace('#{optionsTable}', generateOptionsMarkdown());
// Remove previous readme
if (fs.existsSync(readmePath)) {
fs.unlinkSync(readmePath);
}
// Write new readme
fs.writeFileSync(readmePath, newReadme);
}
});
});
});
}
exports.generateFeaturesDocumentation = generateFeaturesDocumentation;
const README_TEMPLATE = `
# #{featureName}
#{featureDescription} #{Description}
## Example Usage ## Example Usage
\`\`\`json \`\`\`json
"features": { "features": {
"#{nwo}/#{featureId}@#{versionTag}": { "#{Nwo}/#{Id}@#{VersionTag}": {
"version": "latest" "version": "latest"
} }
} }
...@@ -128,9 +54,105 @@ const README_TEMPLATE = ` ...@@ -128,9 +54,105 @@ const README_TEMPLATE = `
## Options ## Options
#{optionsTable} #{OptionsTable}
--- ---
_Note: This file was auto-generated from the [devcontainer-feature.json](./devcontainer-feature.json)._ _Note: This file was auto-generated from the [devcontainer-feature.json](#{RepoUrl})._
`; `;
const TEMPLATE_README_TEMPLATE = `
# #{Name}
#{Description}
## Options
#{OptionsTable}
`;
function generateFeaturesDocumentation(basePath) {
return __awaiter(this, void 0, void 0, function* () {
yield _generateDocumentation(basePath, FEATURES_README_TEMPLATE, 'devcontainer-feature.json');
});
}
exports.generateFeaturesDocumentation = generateFeaturesDocumentation;
function generateTemplateDocumentation(basePath) {
return __awaiter(this, void 0, void 0, function* () {
yield _generateDocumentation(basePath, TEMPLATE_README_TEMPLATE, 'devcontainer-template.json');
});
}
exports.generateTemplateDocumentation = generateTemplateDocumentation;
function _generateDocumentation(basePath, readmeTemplate, metadataFile) {
return __awaiter(this, void 0, void 0, function* () {
const directories = fs.readdirSync(basePath);
yield Promise.all(directories.map((f) => __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c;
if (!f.startsWith('.')) {
const readmePath = path.join(basePath, f, 'README.md');
// Reads in feature.json
const jsonPath = path.join(basePath, f, metadataFile);
if (!fs.existsSync(jsonPath)) {
core.error(`${metadataFile} not found at path '${jsonPath}'`);
return;
}
let parsedJson = undefined;
try {
parsedJson = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
}
catch (err) {
core.error(`Failed to parse ${jsonPath}: ${err}`);
return;
}
if (!parsedJson || !(parsedJson === null || parsedJson === void 0 ? void 0 : parsedJson.id)) {
core.error(`${metadataFile} for '${f}' does not contain an 'id'`);
return;
}
const srcInfo = (0, utils_1.getGitHubMetadata)();
const ref = srcInfo.ref;
const owner = srcInfo.owner;
const repo = srcInfo.repo;
// Add tag if parseable
let versionTag = 'latest';
if (ref && ref.includes('refs/tags/')) {
versionTag = ref.replace('refs/tags/', '');
}
const generateOptionsMarkdown = () => {
const options = parsedJson === null || parsedJson === void 0 ? void 0 : parsedJson.options;
if (!options) {
return '';
}
const keys = Object.keys(options);
const contents = keys
.map(k => {
const val = options[k];
return `| ${k} | ${val.description || '-'} | ${val.type || '-'} | ${val.default || '-'} |`;
})
.join('\n');
return '| Options Id | Description | Type | Default Value |\n' + '|-----|-----|-----|-----|\n' + contents;
};
let urlToConfig = './devcontainer-feature.json';
const basePathTrimmed = basePath.startsWith('./') ? basePath.substring(2) : basePath;
if (srcInfo.owner && srcInfo.repo) {
urlToConfig = `https://github.com/${srcInfo.owner}/${srcInfo.repo}/blob/main/${basePathTrimmed}/${f}/devcontainer-feature.json`;
}
const newReadme = readmeTemplate
// Templates & Features
.replace('#{Id}', parsedJson.id)
.replace('#{Name}', parsedJson.name ? `${parsedJson.name} (${parsedJson.id})` : `${parsedJson.id}`)
.replace('#{Description}', (_a = parsedJson.description) !== null && _a !== void 0 ? _a : '')
.replace('#{OptionsTable}', generateOptionsMarkdown())
// Features Only
.replace('#{Nwo}', `${owner}/${repo}`)
.replace('#{VersionTag}', versionTag)
// Templates Only
.replace('#{ManifestName}', (_c = (_b = parsedJson === null || parsedJson === void 0 ? void 0 : parsedJson.image) === null || _b === void 0 ? void 0 : _b.manifest) !== null && _c !== void 0 ? _c : '')
.replace('#{RepoUrl}', urlToConfig);
// Remove previous readme
if (fs.existsSync(readmePath)) {
fs.unlinkSync(readmePath);
}
// Write new readme
fs.writeFileSync(readmePath, newReadme);
}
})));
});
}
...@@ -44,42 +44,51 @@ function run() { ...@@ -44,42 +44,51 @@ function run() {
core.debug('Reading input parameters...'); core.debug('Reading input parameters...');
// Read inputs // Read inputs
const shouldPublishFeatures = core.getInput('publish-features').toLowerCase() === 'true'; const shouldPublishFeatures = core.getInput('publish-features').toLowerCase() === 'true';
const shouldPublishTemplate = core.getInput('publish-templates').toLowerCase() === 'true'; const shouldPublishTemplates = core.getInput('publish-templates').toLowerCase() === 'true';
const shouldGenerateDocumentation = core.getInput('generate-docs').toLowerCase() === 'true'; const shouldGenerateDocumentation = core.getInput('generate-docs').toLowerCase() === 'true';
// Experimental
const shouldTagIndividualFeatures = core.getInput('tag-individual-features').toLowerCase() === 'true';
const shouldPublishToNPM = core.getInput('publish-to-npm').toLowerCase() === 'true';
const shouldPublishReleaseArtifacts = core.getInput('publish-release-artifacts').toLowerCase() === 'true';
const shouldPublishToOCI = core.getInput('publish-to-oci').toLowerCase() === 'true';
const opts = {
shouldTagIndividualFeatures,
shouldPublishToNPM,
shouldPublishReleaseArtifacts,
shouldPublishToOCI
};
const featuresBasePath = core.getInput('base-path-to-features');
const templatesBasePath = core.getInput('base-path-to-templates');
let featuresMetadata = undefined; let featuresMetadata = undefined;
let templatesMetadata = undefined; let templatesMetadata = undefined;
// -- Package Release Artifacts
if (shouldPublishFeatures) { if (shouldPublishFeatures) {
core.info('Publishing features...'); core.info('Publishing features...');
const featuresBasePath = core.getInput('base-path-to-features'); featuresMetadata = yield packageFeatures(featuresBasePath, opts);
featuresMetadata = yield packageFeatures(featuresBasePath);
} }
if (shouldPublishTemplate) { if (shouldPublishTemplates) {
core.info('Publishing template...'); core.info('Publishing template...');
const basePathToDefinitions = core.getInput('base-path-to-templates'); templatesMetadata = yield packageTemplates(templatesBasePath);
templatesMetadata = undefined; // TODO
yield packageTemplates(basePathToDefinitions);
} }
if (shouldGenerateDocumentation) { // -- Generate Documentation
core.info('Generating documentation...'); if (shouldGenerateDocumentation && featuresBasePath) {
const featuresBasePath = core.getInput('base-path-to-features'); core.info('Generating documentation for features...');
if (featuresBasePath) { yield (0, generateDocs_1.generateFeaturesDocumentation)(featuresBasePath);
yield (0, generateDocs_1.generateFeaturesDocumentation)(featuresBasePath);
}
else {
core.error("'base-path-to-features' input is required to generate documentation");
}
// TODO: base-path-to-templates
} }
// TODO: Programatically add feature/template fino with relevant metadata for UX clients. if (shouldGenerateDocumentation && templatesBasePath) {
core.info('Generation metadata file: devcontainer-collection.json'); core.info('Generating documentation for templates...');
yield (0, utils_1.addCollectionsMetadataFile)(featuresMetadata, templatesMetadata); yield (0, generateDocs_1.generateTemplateDocumentation)(templatesBasePath);
}
// -- Programatically add feature/template metadata to collections file.
core.info('Generating metadata file: devcontainer-collection.json');
yield (0, utils_1.addCollectionsMetadataFile)(featuresMetadata, templatesMetadata, opts);
}); });
} }
function packageFeatures(basePath) { function packageFeatures(basePath, opts) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
try { try {
core.info(`Archiving all features in ${basePath}`); core.info(`Archiving all features in ${basePath}`);
const metadata = yield (0, utils_1.getFeaturesAndPackage)(basePath); const metadata = yield (0, utils_1.getFeaturesAndPackage)(basePath, opts);
core.info('Packaging features has finished.'); core.info('Packaging features has finished.');
return metadata; return metadata;
} }
...@@ -94,14 +103,17 @@ function packageFeatures(basePath) { ...@@ -94,14 +103,17 @@ function packageFeatures(basePath) {
function packageTemplates(basePath) { function packageTemplates(basePath) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
try { try {
core.info(`Archiving all templated in ${basePath}`); core.info(`Archiving all templates in ${basePath}`);
yield (0, utils_1.getTemplatesAndPackage)(basePath); const metadata = yield (0, utils_1.getTemplatesAndPackage)(basePath);
core.info('Packaging templates has finished.'); core.info('Packaging templates has finished.');
return metadata;
} }
catch (error) { catch (error) {
if (error instanceof Error) if (error instanceof Error) {
core.setFailed(error.message); core.setFailed(error.message);
}
} }
return;
}); });
} }
run(); run();
...@@ -35,11 +35,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) { ...@@ -35,11 +35,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod }; return (mod && mod.__esModule) ? mod : { "default": mod };
}; };
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.getTemplatesAndPackage = exports.getFeaturesAndPackage = exports.addCollectionsMetadataFile = exports.tarDirectory = exports.renameLocal = exports.mkdirLocal = exports.writeLocalFile = exports.readLocalFile = void 0; exports.getTemplatesAndPackage = exports.getFeaturesAndPackage = exports.pushCollectionsMetadataToOCI = exports.addCollectionsMetadataFile = exports.getGitHubMetadata = exports.tarDirectory = exports.renameLocal = exports.mkdirLocal = exports.writeLocalFile = exports.readLocalFile = void 0;
const github = __importStar(require("@actions/github")); const github = __importStar(require("@actions/github"));
const tar = __importStar(require("tar")); const tar = __importStar(require("tar"));
const fs = __importStar(require("fs")); const fs = __importStar(require("fs"));
const core = __importStar(require("@actions/core")); const core = __importStar(require("@actions/core"));
const child_process = __importStar(require("child_process"));
const util_1 = require("util"); const util_1 = require("util");
const path_1 = __importDefault(require("path")); const path_1 = __importDefault(require("path"));
exports.readLocalFile = (0, util_1.promisify)(fs.readFile); exports.readLocalFile = (0, util_1.promisify)(fs.readFile);
...@@ -62,23 +63,83 @@ function tarDirectory(path, tgzName) { ...@@ -62,23 +63,83 @@ function tarDirectory(path, tgzName) {
}); });
} }
exports.tarDirectory = tarDirectory; exports.tarDirectory = tarDirectory;
function addCollectionsMetadataFile(featuresMetadata, templatesMetadata) { function getGitHubMetadata() {
// Insert github repo metadata
const ref = github.context.ref;
let sourceInformation = {
owner: github.context.repo.owner,
repo: github.context.repo.repo,
ref,
sha: github.context.sha
};
// Add tag if parseable
if (ref.includes('refs/tags/')) {
const tag = ref.replace('refs/tags/', '');
sourceInformation = Object.assign(Object.assign({}, sourceInformation), { tag });
}
return sourceInformation;
}
exports.getGitHubMetadata = getGitHubMetadata;
function tagFeatureAtVersion(featureMetaData) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const p = path_1.default.join('.', 'devcontainer-collection.json'); const featureId = featureMetaData.id;
// Insert github repo metadata const featureVersion = featureMetaData.version;
const ref = github.context.ref; const tagName = `${featureId}_v${featureVersion}`;
let sourceInformation = { // Get GITHUB_TOKEN from environment
source: 'github', const githubToken = process.env.GITHUB_TOKEN;
if (!githubToken) {
core.setFailed('GITHUB_TOKEN environment variable is not set.');
return;
}
// Setup Octokit client
const octokit = github.getOctokit(githubToken);
// Use octokit to get all tags for this repo
const tags = yield octokit.rest.repos.listTags({
owner: github.context.repo.owner,
repo: github.context.repo.repo
});
// See if tags for this release was already created.
const tagExists = tags.data.some(tag => tag.name === tagName);
if (tagExists) {
core.info(`Tag ${tagName} already exists. Skipping...`);
return;
}
// Create tag
const createdTag = yield octokit.rest.git.createTag({
tag: tagName,
message: `Feature ${featureId} version ${featureVersion}`,
object: github.context.sha,
type: 'commit',
owner: github.context.repo.owner,
repo: github.context.repo.repo
});
if (createdTag.status === 201) {
core.info(`Tagged '${tagName}'`);
}
else {
core.setFailed(`Failed to tag '${tagName}'`);
return;
}
// Create reference to tag
const createdRef = yield octokit.rest.git.createRef({
owner: github.context.repo.owner, owner: github.context.repo.owner,
repo: github.context.repo.repo, repo: github.context.repo.repo,
ref, ref: `refs/tags/${tagName}`,
sha: github.context.sha sha: createdTag.data.sha
}; });
// Add tag if parseable if (createdRef.status === 201) {
if (ref.includes('refs/tags/')) { core.info(`Created reference for '${tagName}'`);
const tag = ref.replace('refs/tags/', '');
sourceInformation = Object.assign(Object.assign({}, sourceInformation), { tag });
} }
else {
core.setFailed(`Failed to reference of tag '${tagName}'`);
return;
}
});
}
function addCollectionsMetadataFile(featuresMetadata, templatesMetadata, opts) {
return __awaiter(this, void 0, void 0, function* () {
const p = path_1.default.join('.', 'devcontainer-collection.json');
const sourceInformation = getGitHubMetadata();
const metadata = { const metadata = {
sourceInformation, sourceInformation,
features: featuresMetadata || [], features: featuresMetadata || [],
...@@ -86,27 +147,148 @@ function addCollectionsMetadataFile(featuresMetadata, templatesMetadata) { ...@@ -86,27 +147,148 @@ function addCollectionsMetadataFile(featuresMetadata, templatesMetadata) {
}; };
// Write to the file // Write to the file
yield (0, exports.writeLocalFile)(p, JSON.stringify(metadata, undefined, 4)); yield (0, exports.writeLocalFile)(p, JSON.stringify(metadata, undefined, 4));
if (opts.shouldPublishToOCI) {
pushCollectionsMetadataToOCI(p);
}
}); });
} }
exports.addCollectionsMetadataFile = addCollectionsMetadataFile; exports.addCollectionsMetadataFile = addCollectionsMetadataFile;
function getFeaturesAndPackage(basePath) { function pushArtifactToOCI(version, featureName, artifactPath) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const exec = (0, util_1.promisify)(child_process.exec);
const versions = [version, '1.0', '1', 'latest']; // TODO: Generate semantic versions from 'version'
const sourceInfo = getGitHubMetadata();
yield Promise.all(versions.map((v) => __awaiter(this, void 0, void 0, function* () {
const ociRepo = `${sourceInfo.owner}/${sourceInfo.repo}/${featureName}:${v}`;
try {
const cmd = `oras push ghcr.io/${ociRepo} \
--manifest-config /dev/null:application/vnd.devcontainers \
./${artifactPath}:application/vnd.devcontainers.layer.v1+tar`;
yield exec(cmd);
core.info(`Pushed artifact to '${ociRepo}'`);
}
catch (error) {
if (error instanceof Error)
core.setFailed(`Failed to push '${ociRepo}': ${error.message}`);
}
})));
});
}
function pushCollectionsMetadataToOCI(collectionJsonPath) {
return __awaiter(this, void 0, void 0, function* () {
const exec = (0, util_1.promisify)(child_process.exec);
const sourceInfo = getGitHubMetadata();
const ociRepo = `${sourceInfo.owner}/${sourceInfo.repo}:latest`;
try {
const cmd = `oras push ghcr.io/${ociRepo} \
--manifest-config /dev/null:application/vnd.devcontainers \
./${collectionJsonPath}:application/vnd.devcontainers.collection.layer.v1+json`;
yield exec(cmd);
core.info(`Pushed collection metadata to '${ociRepo}'`);
}
catch (error) {
if (error instanceof Error)
core.setFailed(`Failed to push collection metadata '${ociRepo}': ${error.message}`);
}
});
}
exports.pushCollectionsMetadataToOCI = pushCollectionsMetadataToOCI;
function loginToGHCR() {
return __awaiter(this, void 0, void 0, function* () {
const exec = (0, util_1.promisify)(child_process.exec);
// Get GITHUB_TOKEN from environment
const githubToken = process.env.GITHUB_TOKEN;
if (!githubToken) {
core.setFailed('GITHUB_TOKEN environment variable is not set.');
return;
}
try {
yield exec(`oras login ghcr.io -u USERNAME -p ${githubToken}`);
core.info('Oras logged in successfully!');
}
catch (error) {
if (error instanceof Error)
core.setFailed(` Oras login failed!`);
}
});
}
function getFeaturesAndPackage(basePath, opts) {
return __awaiter(this, void 0, void 0, function* () {
const { shouldPublishToNPM, shouldTagIndividualFeatures, shouldPublishReleaseArtifacts, shouldPublishToOCI } = opts;
const featureDirs = fs.readdirSync(basePath); const featureDirs = fs.readdirSync(basePath);
let metadatas = []; let metadatas = [];
const exec = (0, util_1.promisify)(child_process.exec);
if (shouldPublishToOCI) {
yield loginToGHCR();
}
yield Promise.all(featureDirs.map((f) => __awaiter(this, void 0, void 0, function* () { yield Promise.all(featureDirs.map((f) => __awaiter(this, void 0, void 0, function* () {
var _a;
core.info(`feature ==> ${f}`); core.info(`feature ==> ${f}`);
if (f !== '.' && f !== '..') { if (!f.startsWith('.')) {
const featureFolder = path_1.default.join(basePath, f); const featureFolder = path_1.default.join(basePath, f);
const archiveName = `${f}.tgz`;
yield tarDirectory(`${basePath}/${f}`, archiveName);
const featureJsonPath = path_1.default.join(featureFolder, 'devcontainer-feature.json'); const featureJsonPath = path_1.default.join(featureFolder, 'devcontainer-feature.json');
if (!fs.existsSync(featureJsonPath)) { if (!fs.existsSync(featureJsonPath)) {
core.error(`Feature ${f} is missing a devcontainer-feature.json`); core.error(`Feature '${f}' is missing a devcontainer-feature.json`);
core.setFailed('All features must have a devcontainer-feature.json'); core.setFailed('All features must have a devcontainer-feature.json');
return; return;
} }
const featureMetadata = JSON.parse(fs.readFileSync(featureJsonPath, 'utf8')); const featureMetadata = JSON.parse(fs.readFileSync(featureJsonPath, 'utf8'));
if (!featureMetadata.id || !featureMetadata.version) {
core.error(`Feature '${f}' is must defined an id and version`);
core.setFailed('Incomplete devcontainer-feature.json');
}
metadatas.push(featureMetadata); metadatas.push(featureMetadata);
const sourceInfo = getGitHubMetadata();
if (!sourceInfo.owner) {
core.setFailed('Could not determine repository owner.');
return;
}
const archiveName = `${f}.tgz`;
// ---- PUBLISH RELEASE ARTIFACTS (classic method) ----
if (shouldPublishReleaseArtifacts || shouldPublishToOCI) {
core.info(`** Tar'ing feature`);
yield tarDirectory(featureFolder, archiveName);
}
// ---- PUBLISH TO NPM ----
if (shouldPublishToOCI) {
core.info(`** Publishing to OCI`);
// TODO: CHECK IF THE FEATURE IS ALREADY PUBLISHED UNDER GIVEN TAG
yield pushArtifactToOCI(featureMetadata.version, f, archiveName);
}
// ---- TAG INDIVIDUAL FEATURES ----
if (shouldTagIndividualFeatures) {
core.info(`** Tagging individual feature`);
yield tagFeatureAtVersion(featureMetadata);
}
// ---- PUBLISH TO NPM ----
if (shouldPublishToNPM) {
core.info(`** Publishing to NPM`);
// Adds a package.json file to the feature folder
const packageJsonPath = path_1.default.join(featureFolder, 'package.json');
// if (!sourceInfo.tag) {
// core.error(`Feature ${f} is missing a tag! Cannot publish to NPM.`);
// core.setFailed('All features published to NPM must be tagged with a version');
// }
const packageJsonObject = {
name: `@${sourceInfo.owner}/${f}`,
version: featureMetadata.version,
description: `${(_a = featureMetadata.description) !== null && _a !== void 0 ? _a : 'My cool feature'}`,
author: `${sourceInfo.owner}`,
keywords: ['devcontainer-features']
};
yield (0, exports.writeLocalFile)(packageJsonPath, JSON.stringify(packageJsonObject, undefined, 4));
core.info(`Feature Folder is: ${featureFolder}`);
// Run npm pack, which 'tars' the folder
const packageName = yield exec(`npm pack ./${featureFolder}`);
if (packageName.stderr) {
core.error(`${packageName.stderr.toString()}`);
}
const publishOutput = yield exec(`npm publish --access public "${packageName.stdout.trim()}"`);
core.info(publishOutput.stdout);
if (publishOutput.stderr) {
core.error(`${publishOutput.stderr}`);
}
}
} }
}))); })));
if (metadatas.length === 0) { if (metadatas.length === 0) {
...@@ -119,23 +301,29 @@ function getFeaturesAndPackage(basePath) { ...@@ -119,23 +301,29 @@ function getFeaturesAndPackage(basePath) {
exports.getFeaturesAndPackage = getFeaturesAndPackage; exports.getFeaturesAndPackage = getFeaturesAndPackage;
function getTemplatesAndPackage(basePath) { function getTemplatesAndPackage(basePath) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
let archives = []; const templateDirs = fs.readdirSync(basePath);
fs.readdir(basePath, (err, files) => { let metadatas = [];
if (err) { yield Promise.all(templateDirs.map((t) => __awaiter(this, void 0, void 0, function* () {
core.error(err.message); core.info(`template ==> ${t}`);
core.setFailed(`failed to get list of templates: ${err.message}`); if (!t.startsWith('.')) {
return; const templateFolder = path_1.default.join(basePath, t);
} const archiveName = `devcontainer-template-${t}.tgz`;
files.forEach(file => { // await tarDirectory(templateFolder, archiveName);
core.info(`template ==> ${file}`); const templateJsonPath = path_1.default.join(templateFolder, 'devcontainer-template.json');
if (file !== '.' && file !== '..') { if (!fs.existsSync(templateJsonPath)) {
const archiveName = `devcontainer-definition-${file}.tgz`; core.error(`Template '${t}' is missing a devcontainer-template.json`);
tarDirectory(`${basePath}/${file}`, archiveName); core.setFailed('All templates must have a devcontainer-template.json');
archives.push(archiveName); return;
} }
}); const templateMetadata = JSON.parse(fs.readFileSync(templateJsonPath, 'utf8'));
}); metadatas.push(templateMetadata);
return archives; }
})));
if (metadatas.length === 0) {
core.setFailed('No templates found');
return;
}
return metadatas;
}); });
} }
exports.getTemplatesAndPackage = getTemplatesAndPackage; exports.getTemplatesAndPackage = getTemplatesAndPackage;
name: "(Release) Release dev container features (v2)" name: "(Release) Release dev container features (v2)"
on: on:
push:
tags:
- "v*"
workflow_dispatch: workflow_dispatch:
jobs: jobs:
deploy: deploy:
if: ${{ github.ref == 'refs/heads/main' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Generate tgz - name: Install Oras
run: |
curl -LO https://github.com/oras-project/oras/releases/download/v0.13.0/oras_0.13.0_linux_amd64.tar.gz
mkdir -p oras-install/
tar -zxf oras_0.13.0_*.tar.gz -C oras-install/
mv oras-install/oras /usr/local/bin/
rm -rf oras_0.13.0_*.tar.gz oras-install/
- name: "Publish features to OCI"
uses: ./.github/devcontainers-action # TODO: Once 'devcontainers/action' is published, use that. uses: ./.github/devcontainers-action # TODO: Once 'devcontainers/action' is published, use that.
with: with:
publish-features: "true" publish-features: "true"
publish-to-oci: "true"
base-path-to-features: "./src" base-path-to-features: "./src"
env:
- name: Remove temporary devcontainer-cli # TODO: Temporary GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: rm -rf ./devcontainer-cli-0*
- name: Get or Create Release at current tag
uses: ncipollo/release-action@v1
with:
allowUpdates: true # Lets us upload our own artifact from previous step
artifactErrorsFailBuild: true
artifacts: "./*.tgz,devcontainer-collection.json"
token: ${{ secrets.GITHUB_TOKEN }}
{ {
"id": "anaconda", "id": "anaconda",
"version": "1.0.0",
"name": "Anaconda", "name": "Anaconda",
"options": { "options": {
"version": { "version": {
...@@ -14,9 +15,5 @@ ...@@ -14,9 +15,5 @@
"containerEnv": { "containerEnv": {
"CONDA_DIR": "/usr/local/conda", "CONDA_DIR": "/usr/local/conda",
"PATH": "${PATH}:${CONDA_DIR}/bin:" "PATH": "${PATH}:${CONDA_DIR}/bin:"
},
"install": {
"app": "",
"file": "install.sh"
} }
} }
\ No newline at end of file
{ {
"id": "aws-cli", "id": "aws-cli",
"version": "1.0.0",
"name": "AWS CLI", "name": "AWS CLI",
"description": "Installs the AWS CLI along with needed dependencies. Useful for base Dockerfiles that often are missing required install dependencies like gpg.", "description": "Installs the AWS CLI along with needed dependencies. Useful for base Dockerfiles that often are missing required install dependencies like gpg.",
"options": { "options": {
...@@ -14,9 +15,5 @@ ...@@ -14,9 +15,5 @@
}, },
"extensions": [ "extensions": [
"AmazonWebServices.aws-toolkit-vscode" "AmazonWebServices.aws-toolkit-vscode"
], ]
"install": { }
"app": "",
"file": "install.sh"
}
}
\ No newline at end of file
{ {
"id": "azure-cli", "id": "azure-cli",
"version": "1.0.0",
"name": "Azure CLI", "name": "Azure CLI",
"description": "Installs the Azure CLI along with needed dependencies. Useful for base Dockerfiles that often are missing required install dependencies like gpg.", "description": "Installs the Azure CLI along with needed dependencies. Useful for base Dockerfiles that often are missing required install dependencies like gpg.",
"options": { "options": {
...@@ -14,9 +15,5 @@ ...@@ -14,9 +15,5 @@
}, },
"extensions": [ "extensions": [
"ms-vscode.azurecli" "ms-vscode.azurecli"
], ]
"install": { }
"app": "",
"file": "install.sh"
}
}
\ No newline at end of file
{ {
"id": "common-utils", "id": "common-utils",
"name": "Common Debian Utilities", "name": "Common Debian Utilities",
"version": "1.0.0",
"description": "Installs a set of common command line utilities, Oh My Zsh!, and sets up a non-root user.", "description": "Installs a set of common command line utilities, Oh My Zsh!, and sets up a non-root user.",
"options": { "options": {
"install_Zsh": { "install_Zsh": {
...@@ -55,9 +56,5 @@ ...@@ -55,9 +56,5 @@
}, },
"extensions": [ "extensions": [
"ms-dotnettools.csharp" "ms-dotnettools.csharp"
], ]
"install": { }
"app": "",
"file": "install.sh"
}
}
\ No newline at end of file
{ {
"id": "desktop-lite", "id": "desktop-lite",
"version": "1.0.0",
"name": "Light-weight Desktop", "name": "Light-weight Desktop",
"description": "Adds a lightweight Fluxbox based desktop to the container that can be accessed using a VNC viewer or the web. GUI-based commands executed from the built-in VS code terminal will open on the desktop automatically.", "description": "Adds a lightweight Fluxbox based desktop to the container that can be accessed using a VNC viewer or the web. GUI-based commands executed from the built-in VS code terminal will open on the desktop automatically.",
"options": { "options": {
...@@ -50,9 +51,5 @@ ...@@ -50,9 +51,5 @@
"entrypoint": "/usr/local/share/desktop-init.sh", "entrypoint": "/usr/local/share/desktop-init.sh",
"containerEnv": { "containerEnv": {
"DISPLAY": ":1" "DISPLAY": ":1"
},
"install": {
"app": "",
"file": "install.sh"
} }
} }
\ No newline at end of file
{ {
"id": "docker-from-docker", "id": "docker-from-docker",
"version": "1.0.0",
"name": "Docker (Docker-from-Docker)", "name": "Docker (Docker-from-Docker)",
"descripton": "Re-use the host docker socket, adding the Docker CLI to a container. Feature invokes a script to enable using a forwarded Docker socket within a container to run Docker commands.", "descripton": "Re-use the host docker socket, adding the Docker CLI to a container. Feature invokes a script to enable using a forwarded Docker socket within a container to run Docker commands.",
"options": { "options": {
...@@ -41,9 +42,5 @@ ...@@ -41,9 +42,5 @@
"target": "/var/run/docker-host.sock", "target": "/var/run/docker-host.sock",
"type": "bind" "type": "bind"
} }
], ]
"install": { }
"app": "",
"file": "install.sh"
}
}
\ No newline at end of file
{ {
"id": "docker-in-docker", "id": "docker-in-docker",
"version": "1.0.0",
"name": "Docker (Docker-in-Docker)", "name": "Docker (Docker-in-Docker)",
"description": "Create child containers *inside* a container, independent from the host's docker instance. Installs Docker extension in the container along with needed CLIs.", "description": "Create child containers *inside* a container, independent from the host's docker instance. Installs Docker extension in the container along with needed CLIs.",
"options": { "options": {
...@@ -42,9 +43,5 @@ ...@@ -42,9 +43,5 @@
"target": "/var/lib/docker", "target": "/var/lib/docker",
"type": "volume" "type": "volume"
} }
], ]
"install": { }
"app": "",
"file": "install.sh"
}
}
\ No newline at end of file
{ {
"id": "dotnet", "id": "dotnet",
"version": "1.0.0",
"name": "Dotnet CLI", "name": "Dotnet CLI",
"description": "Installs the .NET CLI. Provides option of installing sdk or runtime, and option of versions to install. Uses latest version of .NET sdk as defaults to install.", "description": "Installs the .NET CLI. Provides option of installing sdk or runtime, and option of versions to install. Uses latest version of .NET sdk as defaults to install.",
"options": { "options": {
......
{ {
"id": "git-lfs", "id": "git-lfs",
"version": "1.0.0",
"name": "Git Large File Support (LFS)", "name": "Git Large File Support (LFS)",
"description": "Installs Git Large File Support (Git LFS) along with needed dependencies. Useful for base Dockerfiles that often are missing required install dependencies like git and curl.", "description": "Installs Git Large File Support (Git LFS) along with needed dependencies. Useful for base Dockerfiles that often are missing required install dependencies like git and curl.",
"options": { "options": {
...@@ -12,9 +13,5 @@ ...@@ -12,9 +13,5 @@
"default": "latest", "default": "latest",
"description": "Select version of Git LFS to install" "description": "Select version of Git LFS to install"
} }
},
"install": {
"app": "",
"file": "install.sh"
} }
} }
\ No newline at end of file
{ {
"id": "git", "id": "git",
"version": "1.0.0",
"name": "Git (from source)", "name": "Git (from source)",
"description": "Install an up-to-date version of Git, built from source as needed. Useful for when you want the latest and greatest features. Auto-detects latest stable version and installs needed dependencies.", "description": "Install an up-to-date version of Git, built from source as needed. Useful for when you want the latest and greatest features. Auto-detects latest stable version and installs needed dependencies.",
"options": { "options": {
...@@ -17,9 +18,5 @@ ...@@ -17,9 +18,5 @@
"default": true, "default": true,
"description": "Install from PPA if available" "description": "Install from PPA if available"
} }
},
"install": {
"app": "",
"file": "install.sh"
} }
} }
\ No newline at end of file
{ {
"id": "github-cli", "id": "github-cli",
"version": "1.0.0",
"name": "GitHub CLI", "name": "GitHub CLI",
"description": "Installs the GitHub CLI. Auto-detects latest version and installs needed dependencies.", "description": "Installs the GitHub CLI. Auto-detects latest version and installs needed dependencies.",
"options": { "options": {
...@@ -12,9 +13,5 @@ ...@@ -12,9 +13,5 @@
"default": "latest", "default": "latest",
"description": "Select version of the GitHub CLI, if not latest." "description": "Select version of the GitHub CLI, if not latest."
} }
},
"install": {
"app": "",
"file": "install.sh"
} }
} }
\ No newline at end of file
{ {
"id": "go", "id": "go",
"version": "1.0.0",
"name": "Go", "name": "Go",
"description": "Installs Go and common Go utilities. Auto-detects latest version and installs needed dependencies.", "description": "Installs Go and common Go utilities. Auto-detects latest version and installs needed dependencies.",
"options": { "options": {
...@@ -28,9 +29,5 @@ ...@@ -28,9 +29,5 @@
], ],
"securityOpt": [ "securityOpt": [
"seccomp=unconfined" "seccomp=unconfined"
], ]
"install": { }
"app": "",
"file": "install.sh"
}
}
\ No newline at end of file
{ {
"id": "hugo", "id": "hugo",
"version": "1.0.0",
"name": "Hugo", "name": "Hugo",
"options": { "options": {
"version": { "version": {
...@@ -14,9 +15,5 @@ ...@@ -14,9 +15,5 @@
"containerEnv": { "containerEnv": {
"HUGO_DIR": "/usr/local/hugo", "HUGO_DIR": "/usr/local/hugo",
"PATH": "${HUGO_DIR}/bin:${PATH}" "PATH": "${HUGO_DIR}/bin:${PATH}"
},
"install": {
"app": "",
"file": "install.sh"
} }
} }
\ No newline at end of file
{ {
"id": "java", "id": "java",
"version": "1.0.0",
"name": "Java (via SDKMAN!)", "name": "Java (via SDKMAN!)",
"description": "Installs Java, SDKMAN! (if not installed), and needed dependencies.", "description": "Installs Java, SDKMAN! (if not installed), and needed dependencies.",
"options": { "options": {
......
{ {
"id": "kubectl-helm-minikube", "id": "kubectl-helm-minikube",
"version": "1.0.0",
"name": "Kubectl, Helm, and Minkube", "name": "Kubectl, Helm, and Minkube",
"description": "Installs latest version of kubectl, Helm, and optionally minikube. Auto-detects latest versions and installs needed dependencies.", "description": "Installs latest version of kubectl, Helm, and optionally minikube. Auto-detects latest versions and installs needed dependencies.",
"options": { "options": {
...@@ -41,9 +42,5 @@ ...@@ -41,9 +42,5 @@
"target": "/home/vscode/.minikube", "target": "/home/vscode/.minikube",
"type": "volume" "type": "volume"
} }
], ]
"install": { }
"app": "",
"file": "install.sh"
}
}
\ No newline at end of file
{ {
"id": "node", "id": "node",
"version": "1.0.0",
"name": "Node.js (via nvm) and yarn", "name": "Node.js (via nvm) and yarn",
"description": "Installs Node.js, nvm, yarn, and needed dependencies.", "description": "Installs Node.js, nvm, yarn, and needed dependencies.",
"options": { "options": {
......
{ {
"id": "oryx", "id": "oryx",
"version": "1.0.0",
"name": "Oryx", "name": "Oryx",
"description": "Installs the oryx CLI", "description": "Installs the oryx CLI",
"containerEnv": { "containerEnv": {
......
{ {
"id": "php", "id": "php",
"version": "1.0.0",
"name": "PHP", "name": "PHP",
"options": { "options": {
"version": { "version": {
......
{ {
"id": "powershell", "id": "powershell",
"version": "1.0.0",
"name": "PowerShell", "name": "PowerShell",
"description": "Installs PowerShell along with needed dependencies. Useful for base Dockerfiles that often are missing required install dependencies like gpg.", "description": "Installs PowerShell along with needed dependencies. Useful for base Dockerfiles that often are missing required install dependencies like gpg.",
"options": { "options": {
...@@ -13,9 +14,5 @@ ...@@ -13,9 +14,5 @@
"default": "latest", "default": "latest",
"description": "Select or enter a version of PowerShell." "description": "Select or enter a version of PowerShell."
} }
},
"install": {
"app": "",
"file": "install.sh"
} }
} }
\ No newline at end of file
{ {
"id": "python", "id": "python",
"version": "1.0.0",
"name": "Python", "name": "Python",
"description": "Installs the provided version of Python, as well as PIPX, and other common Python utilities. JupyterLab is conditionally installed with the python feature. Note: May require source code compilation.", "description": "Installs the provided version of Python, as well as PIPX, and other common Python utilities. JupyterLab is conditionally installed with the python feature. Note: May require source code compilation.",
"options": { "options": {
......
{ {
"id": "ruby", "id": "ruby",
"version": "1.0.0",
"name": "Ruby (via rvm)", "name": "Ruby (via rvm)",
"description": "Installs Ruby, rvm, rbenv, common Ruby utilities, and needed dependencies.", "description": "Installs Ruby, rvm, rbenv, common Ruby utilities, and needed dependencies.",
"options": { "options": {
......
{ {
"id": "rust", "id": "rust",
"version": "1.0.0",
"name": "Rust", "name": "Rust",
"description": "Installs Rust, common Rust utilities, and their required dependencies", "description": "Installs Rust, common Rust utilities, and their required dependencies",
"options": { "options": {
...@@ -50,9 +51,5 @@ ...@@ -50,9 +51,5 @@
"**/target/**": true "**/target/**": true
}, },
"rust-analyzer.checkOnSave.command": "clippy" "rust-analyzer.checkOnSave.command": "clippy"
},
"install": {
"app": "",
"file": "install.sh"
} }
} }
\ No newline at end of file
{ {
"id": "sshd", "id": "sshd",
"version": "1.0.0",
"name": "SSH server", "name": "SSH server",
"description": "Adds a SSH server into a container so that you can use an external terminal, sftp, or SSHFS to interact with it.", "description": "Adds a SSH server into a container so that you can use an external terminal, sftp, or SSHFS to interact with it.",
"options": { "options": {
...@@ -12,9 +13,5 @@ ...@@ -12,9 +13,5 @@
"description": "Currently unused." "description": "Currently unused."
} }
}, },
"entrypoint": "/usr/local/share/ssh-init.sh", "entrypoint": "/usr/local/share/ssh-init.sh"
"install": { }
"app": "",
"file": "install.sh"
}
}
\ No newline at end of file
{ {
"id": "terraform", "id": "terraform",
"version": "1.0.0",
"name": "Terraform, tflint, and TFGrunt", "name": "Terraform, tflint, and TFGrunt",
"description": "Installs the Terraform CLI and optionally TFLint and Terragrunt. Auto-detects latest version and installs needed dependencies.", "description": "Installs the Terraform CLI and optionally TFLint and Terragrunt. Auto-detects latest version and installs needed dependencies.",
"options": { "options": {
...@@ -42,9 +43,5 @@ ...@@ -42,9 +43,5 @@
"args": [] "args": []
}, },
"azureTerraform.terminal": "integrated" "azureTerraform.terminal": "integrated"
},
"install": {
"app": "",
"file": "install.sh"
} }
} }
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment