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:
# 'features' options
base-path-to-features:
required: false
default: './features/src'
default: ''
description: "Relative path to the 'src' folder containing dev container 'feature(s)'"
# 'template' options
base-path-to-templates:
required: false
default: './templates/src'
default: ''
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:
using: 'node16'
main: 'dist/index.js'
This diff is collapsed.
......@@ -520,7 +520,7 @@ minipass
ISC
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
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
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateFeaturesDocumentation = void 0;
exports.generateTemplateDocumentation = exports.generateFeaturesDocumentation = void 0;
const fs = __importStar(require("fs"));
const github = __importStar(require("@actions/github"));
const core = __importStar(require("@actions/core"));
const path = __importStar(require("path"));
function generateFeaturesDocumentation(basePath) {
return __awaiter(this, void 0, void 0, function* () {
fs.readdir(basePath, (err, files) => {
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}
const utils_1 = require("./utils");
const FEATURES_README_TEMPLATE = `
# #{Name}
#{featureDescription}
#{Description}
## Example Usage
\`\`\`json
"features": {
"#{nwo}/#{featureId}@#{versionTag}": {
"#{Nwo}/#{Id}@#{VersionTag}": {
"version": "latest"
}
}
......@@ -128,9 +54,105 @@ const README_TEMPLATE = `
## 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() {
core.debug('Reading input parameters...');
// Read inputs
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';
// 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 templatesMetadata = undefined;
// -- Package Release Artifacts
if (shouldPublishFeatures) {
core.info('Publishing features...');
const featuresBasePath = core.getInput('base-path-to-features');
featuresMetadata = yield packageFeatures(featuresBasePath);
featuresMetadata = yield packageFeatures(featuresBasePath, opts);
}
if (shouldPublishTemplate) {
if (shouldPublishTemplates) {
core.info('Publishing template...');
const basePathToDefinitions = core.getInput('base-path-to-templates');
templatesMetadata = undefined; // TODO
yield packageTemplates(basePathToDefinitions);
templatesMetadata = yield packageTemplates(templatesBasePath);
}
if (shouldGenerateDocumentation) {
core.info('Generating documentation...');
const featuresBasePath = core.getInput('base-path-to-features');
if (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
// -- Generate Documentation
if (shouldGenerateDocumentation && featuresBasePath) {
core.info('Generating documentation for features...');
yield (0, generateDocs_1.generateFeaturesDocumentation)(featuresBasePath);
}
// TODO: Programatically add feature/template fino with relevant metadata for UX clients.
core.info('Generation metadata file: devcontainer-collection.json');
yield (0, utils_1.addCollectionsMetadataFile)(featuresMetadata, templatesMetadata);
if (shouldGenerateDocumentation && templatesBasePath) {
core.info('Generating documentation for templates...');
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* () {
try {
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.');
return metadata;
}
......@@ -94,14 +103,17 @@ function packageFeatures(basePath) {
function packageTemplates(basePath) {
return __awaiter(this, void 0, void 0, function* () {
try {
core.info(`Archiving all templated in ${basePath}`);
yield (0, utils_1.getTemplatesAndPackage)(basePath);
core.info(`Archiving all templates in ${basePath}`);
const metadata = yield (0, utils_1.getTemplatesAndPackage)(basePath);
core.info('Packaging templates has finished.');
return metadata;
}
catch (error) {
if (error instanceof Error)
if (error instanceof Error) {
core.setFailed(error.message);
}
}
return;
});
}
run();
This diff is collapsed.
name: "(Release) Release dev container features (v2)"
on:
push:
tags:
- "v*"
workflow_dispatch:
jobs:
deploy:
if: ${{ github.ref == 'refs/heads/main' }}
runs-on: ubuntu-latest
steps:
- 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.
with:
publish-features: "true"
publish-to-oci: "true"
base-path-to-features: "./src"
- name: Remove temporary devcontainer-cli # TODO: Temporary
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 }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
{
"id": "anaconda",
"version": "1.0.0",
"name": "Anaconda",
"options": {
"version": {
......@@ -14,9 +15,5 @@
"containerEnv": {
"CONDA_DIR": "/usr/local/conda",
"PATH": "${PATH}:${CONDA_DIR}/bin:"
},
"install": {
"app": "",
"file": "install.sh"
}
}
\ No newline at end of file
}
{
"id": "aws-cli",
"version": "1.0.0",
"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.",
"options": {
......@@ -14,9 +15,5 @@
},
"extensions": [
"AmazonWebServices.aws-toolkit-vscode"
],
"install": {
"app": "",
"file": "install.sh"
}
}
\ No newline at end of file
]
}
{
"id": "azure-cli",
"version": "1.0.0",
"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.",
"options": {
......@@ -14,9 +15,5 @@
},
"extensions": [
"ms-vscode.azurecli"
],
"install": {
"app": "",
"file": "install.sh"
}
}
\ No newline at end of file
]
}
{
"id": "common-utils",
"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.",
"options": {
"install_Zsh": {
......@@ -55,9 +56,5 @@
},
"extensions": [
"ms-dotnettools.csharp"
],
"install": {
"app": "",
"file": "install.sh"
}
}
\ No newline at end of file
]
}
{
"id": "desktop-lite",
"version": "1.0.0",
"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.",
"options": {
......@@ -50,9 +51,5 @@
"entrypoint": "/usr/local/share/desktop-init.sh",
"containerEnv": {
"DISPLAY": ":1"
},
"install": {
"app": "",
"file": "install.sh"
}
}
\ No newline at end of file
}
{
"id": "docker-from-docker",
"version": "1.0.0",
"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.",
"options": {
......@@ -41,9 +42,5 @@
"target": "/var/run/docker-host.sock",
"type": "bind"
}
],
"install": {
"app": "",
"file": "install.sh"
}
}
\ No newline at end of file
]
}
{
"id": "docker-in-docker",
"version": "1.0.0",
"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.",
"options": {
......@@ -42,9 +43,5 @@
"target": "/var/lib/docker",
"type": "volume"
}
],
"install": {
"app": "",
"file": "install.sh"
}
}
\ No newline at end of file
]
}
{
"id": "dotnet",
"version": "1.0.0",
"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.",
"options": {
......
{
"id": "git-lfs",
"version": "1.0.0",
"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.",
"options": {
......@@ -12,9 +13,5 @@
"default": "latest",
"description": "Select version of Git LFS to install"
}
},
"install": {
"app": "",
"file": "install.sh"
}
}
\ No newline at end of file
}
{
"id": "git",
"version": "1.0.0",
"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.",
"options": {
......@@ -17,9 +18,5 @@
"default": true,
"description": "Install from PPA if available"
}
},
"install": {
"app": "",
"file": "install.sh"
}
}
\ No newline at end of file
}
{
"id": "github-cli",
"version": "1.0.0",
"name": "GitHub CLI",
"description": "Installs the GitHub CLI. Auto-detects latest version and installs needed dependencies.",
"options": {
......@@ -12,9 +13,5 @@
"default": "latest",
"description": "Select version of the GitHub CLI, if not latest."
}
},
"install": {
"app": "",
"file": "install.sh"
}
}
\ No newline at end of file
}
{
"id": "go",
"version": "1.0.0",
"name": "Go",
"description": "Installs Go and common Go utilities. Auto-detects latest version and installs needed dependencies.",
"options": {
......@@ -28,9 +29,5 @@
],
"securityOpt": [
"seccomp=unconfined"
],
"install": {
"app": "",
"file": "install.sh"
}
}
\ No newline at end of file
]
}
{
"id": "hugo",
"version": "1.0.0",
"name": "Hugo",
"options": {
"version": {
......@@ -14,9 +15,5 @@
"containerEnv": {
"HUGO_DIR": "/usr/local/hugo",
"PATH": "${HUGO_DIR}/bin:${PATH}"
},
"install": {
"app": "",
"file": "install.sh"
}
}
\ No newline at end of file
}
{
"id": "java",
"version": "1.0.0",
"name": "Java (via SDKMAN!)",
"description": "Installs Java, SDKMAN! (if not installed), and needed dependencies.",
"options": {
......
{
"id": "kubectl-helm-minikube",
"version": "1.0.0",
"name": "Kubectl, Helm, and Minkube",
"description": "Installs latest version of kubectl, Helm, and optionally minikube. Auto-detects latest versions and installs needed dependencies.",
"options": {
......@@ -41,9 +42,5 @@
"target": "/home/vscode/.minikube",
"type": "volume"
}
],
"install": {
"app": "",
"file": "install.sh"
}
}
\ No newline at end of file
]
}
{
"id": "node",
"version": "1.0.0",
"name": "Node.js (via nvm) and yarn",
"description": "Installs Node.js, nvm, yarn, and needed dependencies.",
"options": {
......
{
"id": "oryx",
"version": "1.0.0",
"name": "Oryx",
"description": "Installs the oryx CLI",
"containerEnv": {
......
{
"id": "php",
"version": "1.0.0",
"name": "PHP",
"options": {
"version": {
......
{
"id": "powershell",
"version": "1.0.0",
"name": "PowerShell",
"description": "Installs PowerShell along with needed dependencies. Useful for base Dockerfiles that often are missing required install dependencies like gpg.",
"options": {
......@@ -13,9 +14,5 @@
"default": "latest",
"description": "Select or enter a version of PowerShell."
}
},
"install": {
"app": "",
"file": "install.sh"
}
}
\ No newline at end of file
}
{
"id": "python",
"version": "1.0.0",
"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.",
"options": {
......
{
"id": "ruby",
"version": "1.0.0",
"name": "Ruby (via rvm)",
"description": "Installs Ruby, rvm, rbenv, common Ruby utilities, and needed dependencies.",
"options": {
......
{
"id": "rust",
"version": "1.0.0",
"name": "Rust",
"description": "Installs Rust, common Rust utilities, and their required dependencies",
"options": {
......@@ -50,9 +51,5 @@
"**/target/**": true
},
"rust-analyzer.checkOnSave.command": "clippy"
},
"install": {
"app": "",
"file": "install.sh"
}
}
\ No newline at end of file
}
{
"id": "sshd",
"version": "1.0.0",
"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.",
"options": {
......@@ -12,9 +13,5 @@
"description": "Currently unused."
}
},
"entrypoint": "/usr/local/share/ssh-init.sh",
"install": {
"app": "",
"file": "install.sh"
}
}
\ No newline at end of file
"entrypoint": "/usr/local/share/ssh-init.sh"
}
{
"id": "terraform",
"version": "1.0.0",
"name": "Terraform, tflint, and TFGrunt",
"description": "Installs the Terraform CLI and optionally TFLint and Terragrunt. Auto-detects latest version and installs needed dependencies.",
"options": {
......@@ -42,9 +43,5 @@
"args": []
},
"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