cschleiden / vscode-github-actions

Simple, unofficial extension to view GitHub Actions workflows and runs in VS Code
https://marketplace.visualstudio.com/items?itemName=cschleiden.vscode-github-actions
MIT License
302 stars 35 forks source link

[Proposal] Extension API for automating GitHub Actions workflow creation #160

Open philliphoff opened 2 years ago

philliphoff commented 2 years ago

Description

Deployment of "cloud native" components is becoming an integral component of the development process. GitHub (the website) provides a set of "starter" templates for GitHub Actions workflows, for various deployment scenarios, but there is no current means for this extension (or others) to reuse them. Furthermore, those templates often require additional user action before they can properly function, such as setting repository secrets and/or replacing tokens in the template content.

VS Code extensions have need to provide its users the ability to create deployment assets and automate their configuration. Some extensions will simply want to reuse the GitHub Actions workflow templates as-is, some will want to further automate their configuration, and some will want to offer completely new workflows.

The proposal is to have this extension offer its users (1) the direct means to create GitHub Actions workflows (e.g. via the command palette), both the existing GitHub Actions templates as well as any proffered by 3rd-party extensions, (2) the ability for 3rd-party extensions to further automate the creation of existing GitHub Actions templates, and (3) the ability for 3rd-party extensions to proffer their own workflow templates.

Goals

Non-goals

Extension API

This extension will export a versioned API to be used by 3rd-party extensions, both for initiating the creation of a specific workflow as well as to register itself as a contributor of workflow templates (or augmentor of existing "starter" workflow templates).

import * as vscode from "vscode";

/**
 * Context provided to the GitHub workflow provider when asked to create a workflow.
 */
export interface WorkflowCreationContext {
    /**
     * Optional and opaque context provided by the caller when creating the workflow.
     */
    readonly callerContext?: never;

    /**
     * Initial content (if any) for a new workflow. Populated, for example, when creating a starter workflow.
     */
    readonly content?: string;

    /**
     * Initial name for a new workflow file. Populated, for example, when creating a starter workflow.
     */
    readonly suggestedFileName?: string;

    /**
     * The type of workflow being created. This can be useful when the same handler is used for multiple workflow types.
     */
    readonly type: string;

    /**
     * The URI for the workspace in which the workflow will be created.
     */
    readonly workspaceUri: vscode.Uri;

    /**
     * Creates a new workflow file.
     *
     * @param suggestedFileName The suggested file name for a new workflow.
     * @param content The content of the workflow.
     *
     * @returns The actual URI of the created workflow.
     */
    createWorkflowFile(suggestedFileName: string, content: string): Promise<vscode.Uri | undefined>;

    /**
     * Creates or updates an actions secret in the GitHub repository.
     *
     * @param suggestedName The suggested name for the secret.
     * @param value The new value of the secret.
     *
     * @returns The actual name of the created/updated secret.
     */
    setSecret(suggestedName: string, value: string): Promise<string | undefined>;
}

export interface GitHubWorkflowProvider {
    createWorkflow(context: WorkflowCreationContext): Promise<void>;
}

/**
 * The current (v1) GitHub Actions extension API.
 */
export interface GitHubActionsApi {
    /**
     * Creates a new workflow.
     *
     * @param type The type of workflow to create. If omitted or undefined, the user will be prompted to select a workflow.
     * @param callerContext An optional, opaque object provided to the workflow provider.
     *
     * @returns The URIs of all created workflow files.
     */
    createWorkflow(type?: string, callerContext?: never): Promise<vscode.Uri[]>;

    /**
     * Registers a provider of a specific workflow type.
     *
     * @param type The type of workflow associated with the provider.
     * @param provider The provider that will handle creating workflows of the specified type.
     *
     * @returns A disposable that will unregister the provider.
     */
    registerWorkflowProvider(type: string, provider: GitHubWorkflowProvider): vscode.Disposable;
}

/**
 * Exported object of the GitHub Actions extension.
 */
export interface GitHubActionsApiManager {

    /**
     * Gets a specific version of the GitHub Actions extension API.
     *
     * @typeparam T The type of the API.
     * @param version The version of the API to return. Defaults to the latest version.
     *
     * @returns The requested API or undefined, if not available.
     */
    getApi<T>(version: 1): T | undefined
}

Package Configuration

3rd-party extensions contributing workflows declare them in package.json in one of two ways, depending on whether they are augmenting the existing "starter" workflows or creating their own.

Augmenting "Starter" Workflows

Workflow contributions should include the following properties:

{
    "contributes": {
        "x-github-workflows": [
            {
                "workflow": "deployments/azure-webapps-dotnet-core"
            }
        ]
    }
}

Custom Workflows

Custom workflow contributions should include the following properties:

{
    "contributes": {
        "x-github-workflows": [
            {
                "workflow": "my-deployment-workflow-type",
                "title": "My Deployment Workflow",
                "description": "A workflow to automate deployment of my service type",
                "group": "deployments"
            }
        ]
    }
}

Usage

Creating workflows

import * as vscode from 'vscode';
import { GitHubActionsApi, GitHubActionsApiManager } from 'vscode-github-actions-api';

export function activate(context) {
  context.subscriptions.push(
    vscode.commands.registerCommand(
      "my-extension.workflow.create",
      async () => {
        const gitHubActionsExtension = vscode.extensions.getExtension('cschleiden.vscode-github-actions');

        if (gitHubActionsExtension) {
          await gitHubActionsExtension.activate();

          const manager: GitHubActionsApiManager | undefined = gitHubActionsExtension.exports as GitHubActionsApiManager;

          if (manager) {
            const api: GitHubActionsApi | undefined = manager.getApi(1);

            if (api) {
              const workflowFiles = await api.createWorkflow(`deployments/azure-webapps-dotnet-core`);

              // Open all created workflow files...
              await Promise.all(workflowFiles.map(file => vscode.window.showTextDocument(file)));
            }
          }
      }));
}

Registering workflows

import * as vscode from 'vscode';
import { GitHubActionsApi, GitHubActionsApiManager } from 'vscode-github-actions-api';

export function activate(context) {
    const gitHubActionsExtension = vscode.extensions.getExtension('cschleiden.vscode-github-actions');

    if (gitHubActionsExtension) {
        await gitHubActionsExtension.activate();

        const manager: GitHubActionsApiManager | undefined = gitHubActionsExtension.exports as GitHubActionsApiManager;

        if (manager) {
            const api: GitHubActionsApi | undefined = manager.getApi(1);

            if (api) {
                context.subscriptions.push(
                  api.registerWorkflowProvider(
                    'deployments/azure-webapps-dotnet-core',
                    {
                        createWorkflow: async (context): Promise<void> => {

                        const xml: string = // Get Azure publish profile XML...

                        await context.setSecret('AZURE_WEBAPP_PUBLISH_PROFILE', xml);

                        let content = context.content;

                        // Transform content (e.g. replace `your-app-name` with application name)...

                        await context.createWorkflowFile(context.suggestedFileName ?? 'azure-webapps-dotnet-core.yml', content);
                    }));
            }
         }
      }
}

Concerns and Open Questions

Extension API Definition

What will be the best way for 3rd-party extensions to acquire the TypeScript definitions of this extension's API? Potential options are:

"Starter" workflow template ownership

The proposal assumes that each contributed workflow type is owned by a single extension and this seems reasonable for unique workflow contributions. The proposal allows extensions to also augment the "starter" workflow templates; the proposal assumes (perhaps naively) that each template naturally maps to a single extension. In cases where that doesn't hold, where multiple extensions register providers for the same type of workflow, what should this extension do?

Potential options are:

  1. Last registered wins
  2. First registered wins
  3. Neither registered wins
  4. Have an explicit map of workflow types to extension owners
  5. Explicitly allow multiple registered providers per type (i.e. each is given an opportunity to further augment workflow)
  6. Prompt user to select which extension wins during creation (and, related, allow extension-invoked workflow creation to designate an owner independent of the owners)
  7. Allow extensions to augment certain extension-invoked workflow creation without first requiring registration (i.e. one-off augmentation)

Extension Activation

Because workflow contributions are declarative, this extension need not activate extensions contributing workflows unless and until immediately before their workflows are actually created. However, extensions which contribute workflows need access to this extension's API for registration during their activation. This implies that this extension's activation time will directly contribute to the overall activation time of contributing extensions. That is, care will need to minimize this extension's activation time.

Prototype Branch

https://github.com/philliphoff/vscode-github-actions/tree/philliphoff-create-workflow

Reviewers

cschleiden commented 2 years ago

Thank you for this proposal 🙇 and sorry for the delay so far. I'll take a look in a bit!

cschleiden commented 2 years ago

Thanks for submitting this. Overall the approach makes sense to me. I think eventually we'd have to find a different approach to getting the starter workflows, when testing the branch I quickly blew through my user's rate limit for API calls, but starter workflows did show up 👍

The contribution approach where other extensions can contribute starter workflows also looks good overall.

For the extension itself, I think we'll have to do some design work to find a good entry point. Could be just a ➕ icon on the workflow list, but nicer might be a web-view or some buttons in the sidebar, something for us to figure out.

cc @andrewakim