Skip to content

[GitHubActions] generator: typed workflow_dispatch inputs (string, boolean, number, choice, environment) #386

Description

@avidenic

Description

Fallout can only emit untyped (string) workflow_dispatch inputs. GitHubActionsWorkflowDispatchTrigger.Write (src/Fallout.Common/CI/GitHubActions/Configuration/GitHubActionsWorkflowDispatchTrigger.cs:21-32) writes only description: and required: per input, never type:, default:, or options:. The attribute surface is correspondingly string-only: GitHubActionsAttribute.cs:57-58 exposes only OnWorkflowDispatchOptionalInputs / OnWorkflowDispatchRequiredInputs (plain string-name arrays), mapped onto the trigger at :280-288. There is no way to declare a boolean, number, choice, or environment input, set a per-input default:, or supply the options: list a choice input requires.

GitHub Actions supports five workflow_dispatch input types (string, boolean, number, choice, environment), each with description/required/default, and choice additionally with options. Without typing, a "publish?" flag is a free-text box that arrives as the strings "true"/"false", a "channel" input can't be a validated dropdown, and a target-environment input can't be the environment picker.

This needs a different design from "add more arrays." A per-type pair of Optional*/Required* string arrays cannot express options: (choice) or a per-input default:, and would grow to roughly ten arrays. Model each input instead as a first-class, compile-checked declaration via a repeatable attribute:

public enum GitHubActionsInputType { String, Boolean, Number, Choice, Environment }

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class GitHubActionsInputAttribute : Attribute
{
    public GitHubActionsInputAttribute(string name) => Name = name;
    public string Name { get; }
    public GitHubActionsInputType Type { get; set; } = GitHubActionsInputType.String;
    public bool Required { get; set; }
    public string? Default { get; set; }         // emitted verbatim (e.g. "false", "3", "beta")
    public string[] Options { get; set; } = [];   // required iff Type == Choice
    public string? Description { get; set; }      // defaults to the humanized Name
    public string[] Workflows { get; set; } = []; // optional: scope to one or more [GitHubActions] by name; empty = all dispatch workflows on the class
}

Generator changes:

  1. Collect all GitHubActionsInputAttributes on the build class. For each [GitHubActions] workflow, take the inputs whose Workflows is empty (applies to every dispatch workflow) or contains that workflow's name, so a single input can be shared across any subset of workflows.
  2. In the dispatch trigger, emit description:, required:, type: (omit for string so existing output stays byte-identical), default: when set, and an options: list for choice.
  3. Feed GetImports() so every declared input is still importable as env: NAME: ${{ github.event.inputs.NAME }}, exactly as today.
  4. Validate at generation time: choice requires non-empty Options (and Default must be one of them if set); a number Default must parse as a number; a boolean Default must be true/false; and every name in Workflows must match a declared [GitHubActions] workflow. A bad declaration — including a Workflows typo that matches no workflow — fails the build instead of silently emitting invalid YAML or dropping the input.

Backward compatibility: the existing OnWorkflowDispatchOptionalInputs / OnWorkflowDispatchRequiredInputs arrays keep working unchanged (each is equivalent to a string input via the new attribute), so this is purely additive. It also supersedes the boolean-only arrays first sketched in the early draft of this proposal — boolean is now just Type = Boolean.

Usage Example

The Workflows list lets each input target one or more specific workflows, so inputs can be mixed and matched across a class that declares several [GitHubActions] workflows:

[GitHubActions(
    "release",
    GitHubActionsImage.UbuntuLatest,
    On = [GitHubActionsTrigger.WorkflowDispatch],
    InvokedTargets = [nameof(Pack)])]
[GitHubActions(
    "nightly",
    GitHubActionsImage.UbuntuLatest,
    On = [GitHubActionsTrigger.WorkflowDispatch],
    InvokedTargets = [nameof(Test)])]
// Workflows unset -> emitted to BOTH release and nightly:
[GitHubActionsInput("Verbose", Type = Boolean, Default = "false")]
// shared by a chosen subset:
[GitHubActionsInput("Retries", Type = Number, Default = "3", Workflows = ["release", "nightly"])]
// release only (string / choice / environment):
[GitHubActionsInput("Version", Required = true, Workflows = ["release"])]
[GitHubActionsInput("Channel", Type = Choice, Options = ["alpha", "beta", "stable"], Default = "beta", Workflows = ["release"])]
[GitHubActionsInput("DeployEnvironment", Type = Environment, Required = true, Workflows = ["release"])]
public partial class Build : NukeBuild { }

Resulting YAML — .github/workflows/release.yml gets the full set:

on:
  workflow_dispatch:
    inputs:
      Verbose:
        description: "Verbose"
        required: false
        type: boolean
        default: false
      Retries:
        description: "Retries"
        required: false
        type: number
        default: 3
      Version:
        description: "Version"
        required: true
      Channel:
        description: "Channel"
        required: false
        type: choice
        options:
          - alpha
          - beta
          - stable
        default: beta
      DeployEnvironment:
        description: "Deploy Environment"
        required: true
        type: environment

…while .github/workflows/nightly.yml gets only the inputs scoped to it:

on:
  workflow_dispatch:
    inputs:
      Verbose:
        description: "Verbose"
        required: false
        type: boolean
        default: false
      Retries:
        description: "Retries"
        required: false
        type: number
        default: 3

Scoping note: because a free-standing attribute is declared at class level, each input carries an optional Workflows list to target one or more [GitHubActions] workflows by name; leaving it empty applies the input to every dispatch-triggered workflow on the class. The names are validated against the declared workflows at generation time, so a typo fails the build rather than silently dropping the input. The alternative considered, string-encoded input specs on [GitHubActions] itself (e.g. "Channel:choice:alpha|beta|stable:default=beta"), would scope naturally but is stringly-typed and unvalidated, so we recommend the typed attribute instead.

Alternative

Declare every input as a plain string via OnWorkflowDispatchOptionalInputs / OnWorkflowDispatchRequiredInputs: the manual-run UI shows free-text boxes, conditions must string-compare (if: ${{ github.event.inputs.PublishToNuget == 'true' }}), there are no validated choice dropdowns or environment pickers, and no per-input defaults. The only way to get typed inputs today is hand-editing the generated YAML (overwritten on the next regeneration) or forking the generator. Our POC over NUKE added booleans through extra arrays (GitHubActionsWorkflowExtendedDispatchTrigger), but that array approach does not extend to choice/number/environment, which is exactly why this issue proposes the typed attribute.

Could you help with a pull-request?

Yes.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions