diff options
Diffstat (limited to '.github/devcontainers-action/lib')
-rw-r--r-- | .github/devcontainers-action/lib/generateDocs.js | 13 | ||||
-rw-r--r-- | .github/devcontainers-action/lib/main.js | 82 | ||||
-rw-r--r-- | .github/devcontainers-action/lib/utils.js | 300 |
3 files changed, 75 insertions, 320 deletions
diff --git a/.github/devcontainers-action/lib/generateDocs.js b/.github/devcontainers-action/lib/generateDocs.js index 5db6322..4bbeeed 100644 --- a/.github/devcontainers-action/lib/generateDocs.js +++ b/.github/devcontainers-action/lib/generateDocs.js @@ -56,9 +56,11 @@ const FEATURES_README_TEMPLATE = ` #{OptionsTable} +#{Notes} + --- -_Note: This file was auto-generated from the [devcontainer-feature.json](#{RepoUrl})._ +_Note: This file was auto-generated from the [devcontainer-feature.json](#{RepoUrl}). Add additional notes to a \`NOTES.md\`._ `; const TEMPLATE_README_TEMPLATE = ` # #{Name} @@ -107,8 +109,6 @@ function _generateDocumentation(basePath, readmeTemplate, metadataFile, ociRegis return; } const srcInfo = (0, utils_1.getGitHubMetadata)(); - const owner = srcInfo.owner; - const repo = srcInfo.repo; // Add version let version = 'latest'; const parsedVersion = parsedJson === null || parsedJson === void 0 ? void 0 : parsedJson.version; @@ -131,6 +131,10 @@ function _generateDocumentation(basePath, readmeTemplate, metadataFile, ociRegis .join('\n'); return '| Options Id | Description | Type | Default Value |\n' + '|-----|-----|-----|-----|\n' + contents; }; + const generateNotesMarkdown = () => { + const notesPath = path.join(basePath, f, 'NOTES.md'); + return fs.existsSync(notesPath) ? fs.readFileSync(path.join(notesPath), 'utf8') : ''; + }; let urlToConfig = './devcontainer-feature.json'; const basePathTrimmed = basePath.startsWith('./') ? basePath.substring(2) : basePath; if (srcInfo.owner && srcInfo.repo) { @@ -142,9 +146,10 @@ function _generateDocumentation(basePath, readmeTemplate, metadataFile, ociRegis .replace('#{Name}', parsedJson.name ? `${parsedJson.name} (${parsedJson.id})` : `${parsedJson.id}`) .replace('#{Description}', (_a = parsedJson.description) !== null && _a !== void 0 ? _a : '') .replace('#{OptionsTable}', generateOptionsMarkdown()) + .replace('#{Notes}', generateNotesMarkdown()) // Features Only .replace('#{Registry}', ociRegistry) - .replace('#{Namespace}', namespace == '<owner>/<repo>' ? `${owner}/${repo}` : namespace) + .replace('#{Namespace}', namespace) .replace('#{Version}', version) // 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 : '') diff --git a/.github/devcontainers-action/lib/main.js b/.github/devcontainers-action/lib/main.js index 8c20111..b00b44f 100644 --- a/.github/devcontainers-action/lib/main.js +++ b/.github/devcontainers-action/lib/main.js @@ -39,83 +39,55 @@ Object.defineProperty(exports, "__esModule", { value: true }); const core = __importStar(require("@actions/core")); const generateDocs_1 = require("./generateDocs"); const utils_1 = require("./utils"); +const exec = __importStar(require("@actions/exec")); function run() { return __awaiter(this, void 0, void 0, function* () { core.debug('Reading input parameters...'); // Read inputs const shouldPublishFeatures = core.getInput('publish-features').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'); - const ociRegistry = core.getInput('oci-registry'); - const namespace = core.getInput('features-namespace'); - let featuresMetadata = undefined; - let templatesMetadata = undefined; - // -- Package Release Artifacts + const sourceMetadata = (0, utils_1.getGitHubMetadata)(); + const inputOciRegistry = core.getInput('oci-registry'); + const ociRegistry = inputOciRegistry && inputOciRegistry !== '' ? inputOciRegistry : 'ghcr.io'; + const inputNamespace = core.getInput('namespace'); + const namespace = inputNamespace && inputNamespace !== '' ? inputNamespace : `${sourceMetadata.owner}/${sourceMetadata.repo}`; + const cliDebugMode = core.getInput('devcontainer-cli-debug-mode').toLowerCase() === 'true'; + // -- Publish if (shouldPublishFeatures) { core.info('Publishing features...'); - featuresMetadata = yield packageFeatures(featuresBasePath, opts); - } - if (shouldPublishTemplates) { - core.info('Publishing template...'); - templatesMetadata = yield packageTemplates(templatesBasePath); + yield publishFeatures(featuresBasePath, ociRegistry, namespace, cliDebugMode); } // -- Generate Documentation if (shouldGenerateDocumentation && featuresBasePath) { core.info('Generating documentation for features...'); yield (0, generateDocs_1.generateFeaturesDocumentation)(featuresBasePath, ociRegistry, namespace); } - 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, opts) { +function publishFeatures(basePath, ociRegistry, namespace, cliDebugMode = false) { return __awaiter(this, void 0, void 0, function* () { - try { - core.info(`Archiving all features in ${basePath}`); - const metadata = yield (0, utils_1.getFeaturesAndPackage)(basePath, opts); - core.info('Packaging features has finished.'); - return metadata; - } - catch (error) { - if (error instanceof Error) { - core.setFailed(error.message); - } + // Ensures we have the devcontainer CLI installed. + if (!(yield (0, utils_1.ensureDevcontainerCliPresent)(cliDebugMode))) { + core.setFailed('Failed to install devcontainer CLI'); + return false; } - return; - }); -} -function packageTemplates(basePath) { - return __awaiter(this, void 0, void 0, function* () { try { - 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) { - core.setFailed(error.message); + let cmd = 'devcontainer'; + let args = ['features', 'publish', '-r', ociRegistry, '-n', namespace, basePath]; + if (cliDebugMode) { + cmd = 'npx'; + args = ['-y', './devcontainer.tgz', ...args]; } + const res = yield exec.getExecOutput(cmd, args, { + ignoreReturnCode: true + }); + return res.exitCode === 0; + } + catch (err) { + core.setFailed(err === null || err === void 0 ? void 0 : err.message); + return false; } - return; }); } run(); diff --git a/.github/devcontainers-action/lib/utils.js b/.github/devcontainers-action/lib/utils.js index 38f5604..f0885ce 100644 --- a/.github/devcontainers-action/lib/utils.js +++ b/.github/devcontainers-action/lib/utils.js @@ -31,42 +31,21 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; Object.defineProperty(exports, "__esModule", { value: true }); -exports.getTemplatesAndPackage = exports.getFeaturesAndPackage = exports.pushCollectionsMetadataToOCI = exports.addCollectionsMetadataFile = exports.getGitHubMetadata = exports.tarDirectory = exports.renameLocal = exports.mkdirLocal = exports.writeLocalFile = exports.readLocalFile = void 0; +exports.ensureDevcontainerCliPresent = exports.isDevcontainerCliAvailable = exports.getGitHubMetadata = exports.renameLocal = exports.mkdirLocal = exports.writeLocalFile = exports.readLocalFile = void 0; const github = __importStar(require("@actions/github")); -const tar = __importStar(require("tar")); const fs = __importStar(require("fs")); -const core = __importStar(require("@actions/core")); -const child_process = __importStar(require("child_process")); const util_1 = require("util"); -const path_1 = __importDefault(require("path")); +const core = __importStar(require("@actions/core")); +const exec = __importStar(require("@actions/exec")); exports.readLocalFile = (0, util_1.promisify)(fs.readFile); exports.writeLocalFile = (0, util_1.promisify)(fs.writeFile); exports.mkdirLocal = (0, util_1.promisify)(fs.mkdir); exports.renameLocal = (0, util_1.promisify)(fs.rename); -// Filter what gets included in the tar.c -const filter = (file, _) => { - // Don't include the archive itself. - if (file === './devcontainer-features.tgz') { - return false; - } - return true; -}; -function tarDirectory(path, tgzName) { - return __awaiter(this, void 0, void 0, function* () { - return tar.create({ file: tgzName, C: path, filter }, ['.']).then(_ => { - core.info(`Compressed ${path} directory to file ${tgzName}`); - }); - }); -} -exports.tarDirectory = tarDirectory; function getGitHubMetadata() { // Insert github repo metadata const ref = github.context.ref; - let sourceInformation = { + let metadata = { owner: github.context.repo.owner, repo: github.context.repo.repo, ref, @@ -75,255 +54,54 @@ function getGitHubMetadata() { // Add tag if parseable if (ref.includes('refs/tags/')) { const tag = ref.replace('refs/tags/', ''); - sourceInformation = Object.assign(Object.assign({}, sourceInformation), { tag }); + metadata = Object.assign(Object.assign({}, metadata), { tag }); } - return sourceInformation; + return metadata; } exports.getGitHubMetadata = getGitHubMetadata; -function tagFeatureAtVersion(featureMetaData) { - return __awaiter(this, void 0, void 0, function* () { - const featureId = featureMetaData.id; - const featureVersion = featureMetaData.version; - const tagName = `${featureId}_v${featureVersion}`; - // Get GITHUB_TOKEN from environment - 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, - repo: github.context.repo.repo, - ref: `refs/tags/${tagName}`, - sha: createdTag.data.sha - }); - if (createdRef.status === 201) { - core.info(`Created reference for '${tagName}'`); - } - else { - core.setFailed(`Failed to reference of tag '${tagName}'`); - return; - } - }); -} -function addCollectionsMetadataFile(featuresMetadata, templatesMetadata, opts) { +function isDevcontainerCliAvailable(cliDebugMode = false) { return __awaiter(this, void 0, void 0, function* () { - const p = path_1.default.join('.', 'devcontainer-collection.json'); - const sourceInformation = getGitHubMetadata(); - const metadata = { - sourceInformation, - features: featuresMetadata || [], - templates: templatesMetadata || [] - }; - // Write to the file - yield (0, exports.writeLocalFile)(p, JSON.stringify(metadata, undefined, 4)); - if (opts.shouldPublishToOCI) { - pushCollectionsMetadataToOCI(p); - } - }); -} -exports.addCollectionsMetadataFile = addCollectionsMetadataFile; -function pushArtifactToOCI(version, featureName, artifactPath) { - 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}'`); + let cmd = 'devcontainer'; + let args = ['--version']; + if (cliDebugMode) { + cmd = 'npx'; + args = ['-y', './devcontainer.tgz', ...args]; + } + const res = yield exec.getExecOutput(cmd, args, { + ignoreReturnCode: true, + silent: true + }); + core.info(`Devcontainer CLI version '${res.stdout}' is installed.`); + return res.exitCode === 0; } - catch (error) { - if (error instanceof Error) - core.setFailed(`Failed to push collection metadata '${ociRepo}': ${error.message}`); + catch (err) { + return false; } }); } -exports.pushCollectionsMetadataToOCI = pushCollectionsMetadataToOCI; -function loginToGHCR() { +exports.isDevcontainerCliAvailable = isDevcontainerCliAvailable; +function ensureDevcontainerCliPresent(cliDebugMode = false) { 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; + if (yield isDevcontainerCliAvailable(cliDebugMode)) { + core.info('devcontainer CLI is already installed'); + return true; } - try { - yield exec(`oras login ghcr.io -u USERNAME -p ${githubToken}`); - core.info('Oras logged in successfully!'); + if (cliDebugMode) { + core.error('Cannot remotely fetch CLI in debug mode'); + return false; } - 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); - 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* () { - var _a; - core.info(`feature ==> ${f}`); - if (!f.startsWith('.')) { - const featureFolder = path_1.default.join(basePath, f); - const featureJsonPath = path_1.default.join(featureFolder, 'devcontainer-feature.json'); - if (!fs.existsSync(featureJsonPath)) { - core.error(`Feature '${f}' is missing a devcontainer-feature.json`); - core.setFailed('All features must have a devcontainer-feature.json'); - return; - } - 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); - 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) { - core.setFailed('No features found'); - return; + try { + core.info('Fetching the latest @devcontainer/cli...'); + const res = yield exec.getExecOutput('npm', ['install', '-g', '@devcontainers/cli'], { + ignoreReturnCode: true, + silent: true + }); + return res.exitCode === 0; } - return metadatas; - }); -} -exports.getFeaturesAndPackage = getFeaturesAndPackage; -function getTemplatesAndPackage(basePath) { - return __awaiter(this, void 0, void 0, function* () { - const templateDirs = fs.readdirSync(basePath); - let metadatas = []; - yield Promise.all(templateDirs.map((t) => __awaiter(this, void 0, void 0, function* () { - core.info(`template ==> ${t}`); - if (!t.startsWith('.')) { - const templateFolder = path_1.default.join(basePath, t); - const archiveName = `devcontainer-template-${t}.tgz`; - // await tarDirectory(templateFolder, archiveName); - const templateJsonPath = path_1.default.join(templateFolder, 'devcontainer-template.json'); - if (!fs.existsSync(templateJsonPath)) { - core.error(`Template '${t}' is missing a devcontainer-template.json`); - core.setFailed('All templates must have a devcontainer-template.json'); - return; - } - const templateMetadata = JSON.parse(fs.readFileSync(templateJsonPath, 'utf8')); - metadatas.push(templateMetadata); - } - }))); - if (metadatas.length === 0) { - core.setFailed('No templates found'); - return; + catch (err) { + return false; } - return metadatas; }); } -exports.getTemplatesAndPackage = getTemplatesAndPackage; +exports.ensureDevcontainerCliPresent = ensureDevcontainerCliPresent; |