From 9b0f84a15974274da614d1a1f1e0c39873251579 Mon Sep 17 00:00:00 2001 From: Josh Spicer Date: Wed, 25 Jan 2023 07:52:14 -0800 Subject: [PATCH] Add repo tags for each published tag (#117) * repo tags for each published tag * provide hidden option to pin to CLI tag * remove v * parse new contract from (https://github.com/devcontainers/cli/pull/326) * not fatal, add warning * option to disable repo tagging --- .devcontainer/devcontainer.json | 22 ++++++++++++ .gitignore | 2 ++ action.yml | 5 +++ src/contracts/collection.ts | 5 +++ src/main.ts | 60 ++++++++++++++++++++++++++------- src/utils.ts | 44 ++++++++++++++++++++++-- 6 files changed, 123 insertions(+), 15 deletions(-) create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..8ed7e8d --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,22 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node +{ + "name": "Node.js & TypeScript", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/typescript-node:0-16", + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "yarn" + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.gitignore b/.gitignore index fc7c3c1..139178e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ node_modules +dist + # Rest pulled from https://github.com/github/gitignore/blob/master/Node.gitignore # Logs logs diff --git a/action.yml b/action.yml index ba84b07..a0d4644 100644 --- a/action.yml +++ b/action.yml @@ -22,6 +22,11 @@ inputs: description: >- Validate the schema of metadata files (devcontainer-feature.json) and exit without publishing. (Cannot be combined with any publishing step). + disable-repo-tagging: + required: false + default: 'false' + description: >- + Disables adding a git repo tag for each Feature or Template release. # Feature specific inputs publish-features: required: false diff --git a/src/contracts/collection.ts b/src/contracts/collection.ts index 2bc8d54..fefaeac 100644 --- a/src/contracts/collection.ts +++ b/src/contracts/collection.ts @@ -9,6 +9,11 @@ export interface GitHubMetadata { sha?: string; } +export interface PublishResult { + publishedVersions: string[]; + digest: string; + version: string; +} export interface DevContainerCollectionMetadata { sourceInformation: GitHubMetadata; features: Feature[]; diff --git a/src/main.ts b/src/main.ts index 0c7dfa9..1735fae 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,8 +7,9 @@ import * as core from '@actions/core'; import * as exec from '@actions/exec'; import * as path from 'path'; +import { PublishResult } from './contracts/collection'; import { generateFeaturesDocumentation, generateTemplateDocumentation } from './generateDocs'; -import { ensureDevcontainerCliPresent, getGitHubMetadata, readdirLocal, validateFeatureSchema } from './utils'; +import { addRepoTagForPublishedTag, ensureDevcontainerCliPresent, getGitHubMetadata, readdirLocal, validateFeatureSchema } from './utils'; async function run(): Promise { core.debug('Reading input parameters...'); @@ -44,6 +45,8 @@ async function run(): Promise { const disableSchemaValidationAsError = core.getInput('disable-schema-validation').toLowerCase() === 'true'; const validateOnly = core.getInput('validate-only').toLowerCase() === 'true'; + const disableRepoTagging = core.getInput('disable-repo-tagging').toLowerCase() === 'true'; + // -- Publish if (shouldPublishFeatures && shouldPublishTemplates) { @@ -75,30 +78,60 @@ async function run(): Promise { } if (shouldPublishFeatures) { - core.info('Publishing features...'); - if (!(await publish('feature', featuresBasePath, featuresOciRegistry, featuresNamespace, cliDebugMode))) { - core.setFailed('(!) Failed to publish features.'); + core.info('Publishing Features...'); + const publishedFeatures = await publish('feature', featuresBasePath, featuresOciRegistry, featuresNamespace, cliDebugMode); + if (!publishedFeatures) { + core.setFailed('(!) Failed to publish Features.'); return; } + + // Add repo tag for this version at the current commit. + if (!disableRepoTagging) { + for (const featureId in publishedFeatures) { + const version = publishedFeatures[featureId]?.version; + if (!version) { + core.debug(`No version available for '${featureId}', so no repo tag was added for Feature`); + continue; + } + if (!(await addRepoTagForPublishedTag('feature', featureId, version))) { + continue; + } + } + } } if (shouldPublishTemplates) { - core.info('Publishing templates...'); - if (!(await publish('template', templatesBasePath, templatesOciRegistry, templatesNamespace, cliDebugMode))) { - core.setFailed('(!) Failed to publish templates.'); + core.info('Publishing Templates...'); + const publishedTemplates = await publish('template', templatesBasePath, templatesOciRegistry, templatesNamespace, cliDebugMode); + if (!publishedTemplates) { + core.setFailed('(!) Failed to publish Templates.'); return; } + + // Add repo tag for this version at the current commit. + if (!disableRepoTagging) { + for (const templateId in publishedTemplates) { + const version = publishedTemplates[templateId]?.version; + if (!version) { + core.debug(`No version available for '${templateId}', so no repo tag was added for Feature`); + continue; + } + if (!(await addRepoTagForPublishedTag('template', templateId, version))) { + continue; + } + } + } } // -- Generate Documentation if (shouldGenerateDocumentation && featuresBasePath) { - core.info('Generating documentation for features...'); + core.info('Generating documentation for Features...'); await generateFeaturesDocumentation(featuresBasePath, featuresOciRegistry, featuresNamespace); } if (shouldGenerateDocumentation && templatesBasePath) { - core.info('Generating documentation for templates...'); + core.info('Generating documentation for Templates...'); await generateTemplateDocumentation(templatesBasePath); } } @@ -128,11 +161,11 @@ async function publish( ociRegistry: string, namespace: string, cliDebugMode = false -): Promise { +): Promise<{ [featureId: string]: PublishResult } | undefined> { // Ensures we have the devcontainer CLI installed. if (!(await ensureDevcontainerCliPresent(cliDebugMode))) { core.setFailed('Failed to install devcontainer CLI'); - return false; + return; } try { @@ -145,10 +178,11 @@ async function publish( // Fails on non-zero exit code from the invoked process const res = await exec.getExecOutput(cmd, args, {}); - return res.exitCode === 0; + const result: { [featureId: string]: PublishResult } = JSON.parse(res.stdout); + return result; } catch (err: any) { core.setFailed(err?.message); - return false; + return; } } diff --git a/src/utils.ts b/src/utils.ts index 924a2f0..4bdd346 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -53,9 +53,39 @@ export async function isDevcontainerCliAvailable(cliDebugMode = false): Promise< } } +export async function addRepoTagForPublishedTag(type: string, id: string, version: string): Promise { + const octokit = github.getOctokit(process.env.GITHUB_TOKEN || ''); + const tag = `${type}_${id}_${version}`; + core.info(`Adding repo tag '${tag}'...`); + + try { + await octokit.rest.git.createRef({ + owner: github.context.repo.owner, + repo: github.context.repo.repo, + ref: `refs/tags/${tag}`, + sha: github.context.sha + }); + + await octokit.rest.git.createTag({ + owner: github.context.repo.owner, + repo: github.context.repo.repo, + tag, + message: `${tag}`, + object: github.context.sha, + type: 'commit' + }); + } catch (err) { + core.warning(`Failed to automatically add repo tag, manually tag with: 'git tag ${tag} ${github.context.sha}'`); + core.debug(`${err}`); + return false; + } + + core.info(`Tag '${tag}' added.`); + return true; +} + export async function ensureDevcontainerCliPresent(cliDebugMode = false): Promise { if (await isDevcontainerCliAvailable(cliDebugMode)) { - core.info('devcontainer CLI is already installed'); return true; } @@ -64,14 +94,24 @@ export async function ensureDevcontainerCliPresent(cliDebugMode = false): Promis return false; } + // Unless this override is set, + // we'll fetch the latest version of the CLI published to NPM + const cliVersion = core.getInput('devcontainer-cli-version'); + let cli = '@devcontainers/cli'; + if (cliVersion) { + core.info(`Manually overriding CLI version to '${cliVersion}'`); + cli = `${cli}@${cliVersion}`; + } + try { core.info('Fetching the latest @devcontainer/cli...'); - const res = await exec.getExecOutput('npm', ['install', '-g', '@devcontainers/cli'], { + const res = await exec.getExecOutput('npm', ['install', '-g', cli], { ignoreReturnCode: true, silent: true }); return res.exitCode === 0; } catch (err) { + core.error(`Failed to fetch @devcontainer/cli: ${err}`); return false; } }